Last active
April 5, 2019 20:17
-
-
Save masaeedu/83576f7e20ab8df187af1ef19c01afbb to your computer and use it in GitHub Desktop.
The CPS-ed freer monad
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { adt, match, otherwise } = require("@masaeedu/adt"); | |
| const { | |
| Obj, | |
| Fn, | |
| Arr, | |
| Cont, | |
| implement, | |
| Functor, | |
| Apply, | |
| Chain, | |
| fail | |
| } = require("@masaeedu/fp"); | |
| const { mdo } = require("@masaeedu/do"); | |
| // ##### FREER | |
| // The (CPS-ed) freer monad; a model for mixed effects | |
| // A freer computation is simply a function that accepts some effectful | |
| // callback, and utilizes it to to cause an overall effect. | |
| // The callback consumes "pure results" of type `a` and "effect requests" | |
| // of type `(f x, x -> m r)` (for some `x`), and given either sort of input | |
| // produces some effect `m r`. | |
| // We can think of this callback as translating both final results and int- | |
| // ermediate effect requests (which contain *another* callback to be used | |
| // for continuing) into some useful effect (perhaps console output, or HTTP | |
| // requests, or user visible activity on a web page). | |
| // We'll use an ADT to model the inputs to an effect callback. | |
| // :: type FreerInput f a r = ADT '{ Pure: '[a], Bind: '[f x, x -> r] } | |
| const FreerInput = adt({ Pure: ["a"], Bind: ["f x", "x -> r"] }); | |
| const { Pure, Bind } = FreerInput; | |
| // A freer computation, when given such a "effect translator" callback, simply | |
| // calls it a bunch of times with various effect requests and results, and se- | |
| // quences the emitted effects together. | |
| // :: type Freer f m a = (FreerInput f a (m r) -> m r) -> m r | |
| // Interestingly, freer forms a monad regardless of what properties `f` and `m` | |
| // have. | |
| // :: Monad (Freer f m) | |
| const Freer = (() => { | |
| // We're just going to implement the monad's pure and bind operations and derive | |
| // everything else | |
| const monad = Fn.pipe(Arr.map(implement)([Chain, Apply, Functor, Apply])); | |
| // Given some value `a`, you can always produce a harmless freer computation, | |
| // which will accept the callback and immediately feed it `a` as a result, and | |
| // then turn around and hand you back the effect the callback produced. | |
| // :: a -> Freer f m a | |
| const of = a => Cont.of(Pure(a)); | |
| // We also need a way to take an `amb :: a -> Freer f m b` and sequence it onto | |
| // some `ma :: Freer f m a` to produce a `result :: Freer f m b`. | |
| // When producing `result`, we need to remember that we're actually accepting some | |
| // callback `cb :: FreerInput f b (m r) -> m r` and need to use it in some manner | |
| // to produce a final `m r` effect. How do we do this? | |
| // Well what we can do is take our *previous* computation `ma`, and pass it a | |
| // callback that does the following: | |
| // - When the computation passes us a result `a`, we take our `amb` function, | |
| // apply `a` to it to obtain a `Freer f m b` computation, feed it `cb`, and | |
| // boom! we have an `m r` we can return. | |
| // - When the computation passes us an effect processing request, we just use | |
| // `cb` to interpret it! Don't we need to do some translation between our | |
| // `cb :: FreerInput f b (m r) -> m r` and the effect processing request, | |
| // which after all is of type `FreerInput f a (m r)`? | |
| // The beauty part here is that the `Bind` case of a `FreerInput f x (m r)` | |
| // works just as well as the `Bind` case of a `FreerInput f y (m r)`; it | |
| // simply doesn't mention or care about the second type parameter. | |
| // This makes sense, because we're just pushing all the business of actually | |
| // interpreting effect requests into proper effects straight to whomever fi- | |
| // nally supplies us with a callback; we don't care what `m` or `r` they end | |
| // up interpreting everything into. | |
| // :: (a -> Freer f m b) -> Freer f m a -> Freer f m b | |
| const chain = amb => | |
| Cont.chain( | |
| match({ | |
| Pure: amb, | |
| [otherwise]: Cont.of | |
| }) | |
| ); | |
| // :: f (Freer f m a) -> Freer f m a | |
| const wrap = fr => cb => cb(Bind(fr)(ft => ft(cb))); | |
| // :: Functor f -> f a -> Freer f m a | |
| const liftF = F => fa => wrap(F.map(of)(fa)); | |
| // :: Functor f -> Monad m -> (f (m a) -> m a) -> Freer f m a -> m a | |
| const iterT = F => M => phi => m => | |
| m( | |
| match({ | |
| // :: a -> m a | |
| Pure: M.of, | |
| // :: f x -> (x -> m r) -> m a | |
| Bind: f => xmr => phi(F.map(xmr)(f)) | |
| }) | |
| ); | |
| return { ...monad({ of, chain }), wrap, liftF, iterT }; | |
| })(); | |
| // ##### EFFECT REQUESTS | |
| // # STATE | |
| // :: type State s r = ADT '{ Get: '[s -> r], Put: '[s, r] } | |
| const State = adt({ Get: ["s -> r"], Put: ["s", "r"] }); | |
| const { Get, Put } = State; | |
| // :: Functor (State s) | |
| State.map = f => | |
| match({ | |
| Get: t => Get(s => f(t(s))), | |
| Put: s => a => Put(s)(f(a)) | |
| }); | |
| // :: Freer (State s) m s | |
| const get = Freer.liftF(State)(Get(Fn.id)); | |
| // :: s -> Freer (State s) m () | |
| const put = s => Freer.liftF(State)(Put(s)(undefined)); | |
| // # ERROR | |
| // :: type Error e a = ADT '{ Fail: '[e] } | |
| const Error = adt({ Fail: ["e"] }); | |
| const { Fail } = Error; | |
| // :: Functor (Error e) | |
| Error.map = _ => x => x; | |
| // :: e -> Freer (Error e) m () | |
| const error = e => Freer.liftF(Error)(Fail(e)); | |
| // ##### FUNCTOR UNION | |
| // :: (Functor <$> (fs :: [* -> *])) -> Functor (Union fs) | |
| const Union = Fs => { | |
| const index = Arr.foldMap(Obj)(F => Obj["<$"](F)(F.def))(Fs); | |
| const map = f => v => index[v.label].map(f)(v); | |
| return { map }; | |
| }; | |
| // ##### TESTING | |
| // # COMPUTATION | |
| // We're going to ask the user their name, and provided they don't have | |
| // some weird name, we're going to greet them | |
| const weirdnames = ["Wurtzelpfoof"]; // Apologies to all the Wurtzelpfoofs out there | |
| // :: Freer (Union [State String, Error String]) m () | |
| const computation = mdo(Freer)(({ name }) => [ | |
| () => put("Hi! I'm Telly the teletype! Who are you?"), | |
| [name, () => get], | |
| () => | |
| weirdnames.includes(name) | |
| ? error(`${name}? What kind of name is that?`) | |
| : Freer.of(undefined), | |
| () => put(`Hello, ${name}. Nice to meet you.`) | |
| ]); | |
| // # INTERPRETATION UTILS | |
| // :: String -> Cont! () | |
| const log = msg => cb => { | |
| console.log(msg); | |
| cb(undefined); | |
| }; | |
| // :: String -> Cont! a | |
| const panic = str => _ => fail(str); | |
| // # INTERPRETATION | |
| // :: Functor (Union '[State s, Error e]) | |
| const F = Union([State, Error]); | |
| // :: Freer (Union '[State String, Error String]) Cont! :~> Cont! | |
| const main = Freer.iterT(F)(Cont)( | |
| match({ | |
| // :: (String -> Cont! r) -> Cont! r | |
| Get: f => f("Sally"), | |
| // also try f => f("Wurtzelpfoof") | |
| // :: String -> Cont! r -> Cont! r | |
| Put: s => m => Cont["*>"](log(s))(m), | |
| // :: String -> Cont! r | |
| Fail: panic | |
| }) | |
| ); | |
| main(computation)(_ => {}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment