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.