Skip to content

Instantly share code, notes, and snippets.

@masaeedu
Last active January 13, 2019 22:20
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save masaeedu/bd7f32c53ea8c34e30ad0f0f8a7948ca to your computer and use it in GitHub Desktop.
Save masaeedu/bd7f32c53ea8c34e30ad0f0f8a7948ca to your computer and use it in GitHub Desktop.
Interpreting callback APIs as pure, monad-returning functions
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
// 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