Last active
January 13, 2019 22:20
-
-
Save masaeedu/bd7f32c53ea8c34e30ad0f0f8a7948ca to your computer and use it in GitHub Desktop.
Interpreting callback APIs as pure, monad-returning functions
This file contains 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 Cont = require("@masaeedu/fp/dist/instances/cont"); | |
const S = require("sanctuary"); | |
// > It is often said that callbacks do not compose. While this may | |
// > be true, functions that *accept* callbacks compose extraordin- | |
// > arily well. In fact, such functions, which I'll hereafter refer | |
// > to as "continuations", form a monad, and a rather powerful and | |
// > versatile monad at that. | |
// > | |
// > In this snippet, I'll try to demonstrate how the continuation | |
// > monad can be used to work with simple Node APIs that accept ca- | |
// > llbacks in a composable and pure fashion. JS has many APIs that | |
// > accept callbacks, or "return continuations", however you prefer | |
// > to look at it, and the continuation monad gives us another tool | |
// > in our belt for working with these. | |
// > | |
// > You might notice along the way that some APIs (e.g. `setTimeout`) | |
// > need to be finagled into a continuation-returning form. In some | |
// > hypothetical JS library where all these APIs always accepted the | |
// > callback last, as a separate, curried, unary argument, writing | |
// > code that uses the continuation monad would become even easier. | |
// > | |
// > A runnable version of this code is available at: | |
// > https://runkit.com/masaeedu/5b19afa3b1c49900125c866c | |
// :: Milliseconds -> Continuation () () | |
const delay = d => cb => { setTimeout(cb, d); } | |
// :: l -> Continuation x (Either l r) | |
const fail = l => Cont.of(S.Left(l)); | |
// :: r -> Continuation x (Either l r) | |
const succeed = r => Cont.of(S.Right(r)); | |
// :: Milliseconds -> Nat -> Continuation () (Either l r) -> Continuation () (Either l r) | |
const retry = d => n => cnt => { | |
const reattempt = Cont.chain(_ => retry(d)(n - 1)(cnt))(delay(d)); | |
const proceed = S.either | |
(err => n - 1 === 0 ? fail(err) : reattempt) | |
(succeed); | |
return Cont.chain(proceed)(cnt); | |
}; | |
let i = 0; | |
// Simulate a flaky operation | |
// :: Continuation r (Either String String) | |
const flaky = cb => { | |
console.log(`Attempt # ${++i}`); | |
return Math.random() < 0.6 ? cb(S.Left("whoops")) : cb(S.Right("success!")); | |
}; | |
// :: Continuation r (Either String String) | |
const slightlyMoreReliable = retry(1000)(3)(flaky) | |
// Impurely run it | |
slightlyMoreReliable(x => { | |
console.log(S.show(x)); | |
}); | |
// > This will either result in something like: | |
// => "Attempt # 1" | |
// => "Right (\"success!\")" | |
// > or: | |
// => "Attempt # 1" | |
// => "Attempt # 2" | |
// => "Attempt # 3" | |
// => "Left (\"whoops\")" | |
// > Extra reading: http://blog.sigfpe.com/2008/12/mother-of-all-monads.html | |
// > https://en.wikibooks.org/wiki/Haskell/Continuation_passing_style#The_Cont_monad |
This file contains 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
// Monad instance for continuations, i.e. functions that accept functions that accept some data | |
// type Continuation r a = (a -> r) -> r | |
// instance Monad (Cont r) where | |
const Cont = (() => { | |
// Monad | |
// :: x -> ((x -> r) -> r) | |
const of = x => cb => cb(x); | |
// :: (a -> ((b -> r) -> r)) | |
// :: -> ((a -> r) -> r) | |
// :: -> ((b -> r) -> r) | |
const chain = f => cont => cb => cont(a => f(a)(cb)); | |
return { of, chain } | |
})() | |
export default Cont |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment