satgo1546’s ocean

Any sufficiently primitive magic is indistinguishable from technology.

Monads are like promises

😾 Since monads are not a good choice as topic for one's first Haskell blog entry, this post is doomed to fail. It is advised to stop reading now and turn to proper introductory materials instead. I recommend Learn You a Haskell for Great Good! § Input and Output.

Nonetheless, there's a reason why monad tutorials are an evergreen business. Note that the book chapter recommended above has not a single mention of monads (other than in the package name Control.Monad), let alone category theory.

Merely monadic

Merely thenable

Adapted and abridged from Merely monadic, Monad, and Monads as computation

Introduction

Introduction

In Haskell, monadic types — types having an instance for the Monad class — can be thought of as abstract descriptors of computations which are inherently composable — smaller monadic expressions (actions) can be used to build larger ones.

In JavaScript, Promise implementations — classes implementing the PromiseLike interface — can be thought of as abstract descriptors of computations which are inherently composable — smaller promises can be used to build larger ones.

This monadic interface (as specified by Monad) provides actions additional flexibility in separating:

This thenable interface (as specified by PromiseLike) provides actions additional flexibility in separating:

  • the time of composition: when it is defined;

  • the time of execution: when it is used;

  • the mode of computation: what else it can do, in addition to emitting its single (hence the name) output value.

  • the time of composition: when it is defined;

  • the time of execution: when it is used;

  • the mode of computation: what else it can do, in addition to specify what to do after (hence the name) the resolved value is known.

Monadic operations

Thenable methods

Ideally, each monadic type in Haskell should satisfy the monad laws by providing two basic operations:

Ideally, each Promise implementation in JavaScript should comply with Promises/A+ specification by providing two basic methods:

  • return :: Monad m => a -> m a:

    argument :: a another Haskell value
    result :: m a an action, merely returning the argument's value.
  • (>>=) :: Monad m => m a -> (a -> m b) -> m b:

argument #1 :: m a an action
argument #2 :: (a -> m b) a suitable Haskell function (a reaction)
result :: m b another action, the result of binding the output value of argument #1 (the action) to the input of argument #2 (the reaction).
  • Promise.resolve(value: T): Promise<T>:

    argument : T a JavaScript value
    result : Promise<T> a promise, already resolved with the argument's value.
  • Promise.prototype.then<T, TResult>(this: Promise<T>, onFulfilled: (value: T) => TResult): Promise<TResult>:

this argument : Promise<T> an action
argument : (value: T) => TResult a suitable JavaScript function (a callback)
result : Promise<TResult>* another promise, first waiting for this (the promise) to resolve, then calling the argument (the callback) with the resolved value.

Together, these two operations allow many of the specific (and often-repetitive) details of the computational processes used by actions to be abstracted away. This often makes the monadic interface the preferred choice for abstract data types, where the inner workings of monadic actions must be kept private e.g. to ensure they work as intended.

Together, these two methods allow many of the specific (and often-repetitive) details of the computational processes used by actions to be abstracted away. This often makes the monadic interface the preferred choice for abstract data types, where the inner workings of monadic actions must be kept private e.g. to ensure they work as intended.

As a reaction uses its argument to build a new monadic result, it can be thought of as an abstract constructor for new actions. In this way return can then be seen as the trivial-action constructor for new actions: the monadic counterpart to the identity function Prelude.id:

As an onFulfilled callback uses its argument to build a new promise, it can be thought of as an abstract constructor for new promises. In this way Promise.resolve can then be seen as the trivial constructor for new promises: the promise counterpart to the identity function id:

id   :: a -> a
id x =  x
function id<T>(x: T): T {
  return x
}

IO, in particular

Top level, in particular

The IO type is special because a Haskell program is one large IO action (usually) called main — for any other IO action to work, it should be part of the chain of I/O actions composed together to form main. Execution of a Haskell program then starts by the Haskell implementation calling main, according to the implementation's private definition of the abstract IO type.

Top-level code is special because a JavaScript module is one large promise — for any other promise to work, it should be constructed in a function called directly or indirectly from top-level. Execution of a JavaScript module then starts by the JavaScript engine running top-level code, according to the implementation's private definition of the Promise type.

Using monadic actions

Using promises

Assuming:

Assuming:

  • M is a monadic type;

  • x :: a is some other Haskell value;

  • m :: M a is an action;

  • k :: (a -> M b) is a reaction;

  • M, x, m, k are well-behaved;

  • Promise is a Promise implementation;

  • x: T is some other JavaScript value;

  • m: Promise<T> is a promise;

  • k: (value: T) => Promise<TResult> is a callback;

  • M, x, m, k are well-behaved;

then:

then:

(a) r1 = k $ x :: M b (a new action)
(b) r2 = return x :: M a (an action whose output value is x)
(c) r3 = m >>= k :: M b (another new action)
(d) r4 = k =<< m :: M b (the same as r3)
(e) r5 = k $ m (*types don't match*)
(f) r6 = k <$> m :: M (M b) (an action whose output value is also the same as r3)
(g) r7 = join (k <$> m) :: M b (the same as r3)
(a) r1 = k(x) : Promise<TResult> (a new promise)
(b) r2 = Promise.resolve(x) : Promise<T> (a promise whose resolved value is x)
(c) r3 = m.then(k) : Promise<TResult> (another new promise)
(d) r4 = Promise.prototype.then.call(m, k) : Promise<TResult> (the same as r3)
(e) r5 = k(m) (*type error*)
(f) r6 = fmap(k, m) : Promise<Promise<T>> (a promise whose resolved value is also the same as r3)
(g) r7 = join(fmap(k, m)) : Promise<T> (the same as r3)

where:

where:

infixr 0 $
($)     :: (a -> b) -> a -> b
f $ x   =  f x

infixr 1 =<<
(=<<)   :: (a -> M b) -> M a -> M b
k =<< m =  m >>= k

infixl 4 <$>
(<$>)   :: (a -> b) -> M a -> M b
f <$> m =  m >>= \x -> return (f x)

join    :: M (M a) -> M a
join m  =  m >>= \x -> x
function fmap<T, TResult>(callback: (value: T) => TResult, promise: Promise<T>): (promise: Promise<T>) => Promise<TResult> {
  return promise.then(value => Promise.resolve(callback(value)))
}

function join(promise: Promise<Promise<T>>): Promise<T> {
  return promise.then(inner => inner)
}

Even though it is a purely-functional language, Haskell can facilitate the use of effect-based computations by presenting them as monadic actions (using a suitable type), with the monadic interface helping to maintain purity. This is the result of the aforementioned separation of concerns, most notably the time of composition. Monadic actions thus resemble programs in a particular EDSL (embedded domain-specific language), "embedded" because they are legitimate Haskell values which describe impure computations — no extraneous annotations are needed. This is how monadic types in Haskell help keep the pure and the impure apart. But not all monadic actions are impure — for them, the benefits of separating concerns are often combined with the automatic formation of a computational "pipeline".

Even though it is a single-threaded language, JavaScript can facilitate the use of asynchronous computations by presenting them as promises (using a suitable type), with the promise interface helping to maintain purity. This is the result of the aforementioned separation of concerns, most notably the time of composition. Promises thus resemble programs in a particular EDSL (embedded domain-specific language), "embedded" because they are legitimate JavaScript values which describe asynchronous computations — no extraneous annotations are needed. This is how promises in JavaScript help keep the synchronous and the asynchronous apart. But not all promises are asynchronous — for them, the benefits of separating concerns are often combined with the automatic formation of a computational "pipeline".

Because they are very useful in practice but rather confronting for beginners, numerous monad tutorials (including this one) exist to help with the learning process.

Because they are very useful in practice but rather confronting for beginners, numerous promise tutorials (including this one) exist to help with the learning process.

do-notation

async function

In order to improve the look of code that uses monads, Haskell provides a special form of syntactic sugar called do-notation. For example, the following expression:

In order to improve the look of code that uses promises, JavaScript provides a special form of syntactic sugar called async function. For example, the following expression:

thing1 >>= (\x -> func1 x >>= (\y -> thing2 
       >>= (\_ -> func2 y >>= (\z -> return z))))
thing1.then(x => func1(x).then(y => thing2
      .then(_ => func2(y).then(z => Promise.resolve(z)))))

which can be written more clearly by breaking it into several lines and omitting parentheses:

which can be written more clearly by breaking it into several lines:

thing1  >>= \x ->
func1 x >>= \y ->
thing2  >>= \_ ->
func2 y >>= \z ->
return z
thing1  .then(x =>
func1(x).then(y =>
thing2  .then(_ =>
func2(y).then(z =>
Promise.resolve(z)))))

can also be written using do-notation:

can also be written using an async function:

do {
  x <- thing1 ;
  y <- func1 x ;
  thing2 ;
  z <- func2 y ;
  return z
  }
(async () => {
  const x = await thing1;
  const y = await func1(x);
  await thing2;
  const z = await func2(y);
  return Promise.resolve(z);
})()

(the curly braces and the semicolons are optional when the indentation rules are observed).

(the async function keywords are allowed in addition to the arrow syntax).

It is possible to intermix the do-notation with regular notation.

It is possible to intermix async functions with regular functions.

Code written using do-notation is transformed by the compiler to ordinary expressions that use the functions from the Monad class (i.e. the two varieties of bind: (>>=) and (>>)).

Code written using async functions is transformed by the transpiler to ordinary expressions that use the methods from the Promise class (i.e. Promise.prototype.then).

The basic mechanical translation for the do-notation is as follows:

The basic mechanical translation for async functions is as follows:

do { x } = x

do { x ; <stmts> }
  = x >> do { <stmts> }

do { v <- x ; <stmts> }
  = x >>= \v -> do { <stmts> }

do { let <decls> ; <stmts> }
  = let <decls> in do { <stmts> }
async () => { return x }
  = () => { return x }

async () => { await x; ... }
  = () => x.then(async () => { ... })

async () => { const v = await x; ... }
  = () => x.then(async v => { ... })

async () => { x; ... }
  = () => { x; return (async () => { ... })() }

When using do-notation and a monad like State or IO, programs in Haskell look very much like programs written in an imperative language as each line contains a statement that can change the simulated global state of the program and optionally binds a (local) variable that can be used by the statements later in the code block.

When using async functions, asynchronous programs in JavaScript look very much like synchronous programs written for a blocking API as each line contains a statement that waits for the operation to complete and obtains the result immediately.

This gives monadic computations a bit of an imperative feel, but it's important to remember that the monad in question gets to decide what the combination means, and so some unusual forms of control flow might actually occur. In some monads (like parsers, or the list monad), "backtracking" may occur, and in others, even more exotic forms of control might show up (for instance, first-class continuations, or some form of parallelism).

Promises are intentionally limited in that they resolve at most once, so that the synchronous-looking async functions behave reasonably. Arrays, for example, have a method called flatMap which may invoke the callback many times. Even if flatMap were named then, arrays can't be taken as promises as they violate the rules of promises. But imagine promises without such limitations.