As programmers we occasionally find ourselves in “Programmer’s Hell”, where our regular abstractions fail to satisfactory solve certain recurrent problems.
In this post we’ll have a look at some instances of such sitations,
their “ad hoc” solutions provided at the language level, and finally
at how these problems can be solved in a uniform way using Monads. (Call
you language implementor and ask for do
-notation today!)
Null-checking Hell typically occurs when several partial functions, i.e. functions that may not return a real value, need to be run in sequence.
Such functions tend lead to deeply nested and hard to read code with excessive syntactic clutter, obscuring our actual intentions.
var a = getData();
if (a != null) {
var b = getMoreData(a);
if (b != null) {
var c = getMoreData(b);
if (c != null) {
var d = getEvenMoreData(a, c)
if (d != null) {
print(d);
}
}
}
}
Elvis operators introduce a specialized syntax for partial navigation, helping to deal with such issues. Unfortunately this syntax is needlessly complected with object-oriented-style record and method access.
var a = getData();
var b = a?.getMoreData();
var c = b?.getMoreData();
var d = c?.getEvenMoreData(a);
print(d);
By letting our simple functions explicitly return values of the
Maybe
(sometimes Option
) type, we can chain together such
functions using
do
-notation,
making use of the fact that Maybe
/Option
are Monadic.
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
For-loop Hell occurs when iteration through multiple dependent data sets is needed. Just as for null-checking, our code becomes deeply nested, with a lot of syntactic clutter and needless bookkeeping.
var a = getData();
for (var a_i in a) {
var b = getMoreData(a_i);
for (var b_j in b) {
var c = getMoreData(b_j);
for (var c_k in c) {
var d = getMoreData(c_k);
for (var d_l in d) {
print(d_l);
}
}
}
}
A more elegant solution to the problem is found by introducing a
specialized syntactic construction called list-comprehensions, sharing
a lot of similarities with SQL
.
[
print(d)
for a in getData()
for b in getMoreData(a)
for c in getMoreData(b)
for d in getEvenMoreData(a, c)
]
We note that lists are Monads. By reusing do
-notation we can write
an equally elegant solution to our problem without introducing
additional notation.
List comprehensions often include syntax for filtering, which can be
added also to our case by using simple functions such
as
guard
.
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
The most famous and perhaps most painful circle of coding inferno is Callback Hell, where the inversion of control needed to implement asynchronous control leads to deeply nested code and excessive syntactic clutter, difficult to follow error handling and a host of other ailments.
getData(a =>
getMoreData(a, b =>
getMoreData(b, c =>
getEvenMoreData(a, c, d =>
print(d),
err => onErrorD(err)
)
err => onErrorC(err)
),
err => onErrorB(err)
),
err => onErrorA(err)
)
In order to overcome such difficulties, another kind of specialized
syntax is introduced, called async/await
can be introduced. Such
notation typically delegates error handling to existing try/catch
syntax, which can sometimes feel like a hell on it’s own.
async function() {
var a = await getData
var b = await getMoreData(a)
var c = await getMoreData(b)
var d = await getEvenMoreData(a, c)
print(d)
}
Another possible solution is to use Promises (also Futures/Tasks). While problems with nesting are alleviated, using the result of a Promise in multiple places forces us to manually introduce a lexical scope where such a value can be passed around. This leads to one level of nesting per variable that is used in multiple positions.
Using promises directly using .then
-syntax is also often not quite
as clean or clear as using async/await
notation.
getData().then(a => getMoreData(a)
.then(b => getMoreData(b))
.then(c => getEvenMoreData(a, c))
.then(d => print(d)
);
At this point it shouldn’t be surprising that we can solve this problem in exactly the same way as the two situations previously encountered by noting that Promises form Monads. In this context, we often use the word Continuation in place of the above mentioned names. [1]
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
The purely functional world is not without it’s problems, even when side-effects are not a concern. When writing certain kinds of purely functional code, excessive parameter passing between functions can become an issue.
let
(a, st1) = getData initalState
(b, st2) = getMoreData (a, st1)
(c, st3) = getMoreData (b, st2)
(d, st4) = getEvenMoreData (a, c, st3)
in print(d)
We can solve such problems by introducing implicit state that let functions communicate information between without having to pass all dependent values as explicit parameters. Unfortunately by using an imperative model by default severely complicates reasoning about code. Lifetime and size of state typically has no static bounds.
a = getData();
b = getMoreData(a);
c = getMoreData(b);
d = getEvenMoreData(a, c);
print(d)
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
The State Monad provides purely functional state without references,
allowing for many useful higher-order operations on state, such as
easily serializing the state or implementing functions such as
excursion
. ,
similar to what be accomplished in principled state management
libraries such as Redux.
The ST
Monad provides more performant state with references, at the
cost of some higher order behaviour.
Both the State and ST monads bound the lifetime of a stateful computation, ensuring that programs remain easily reasoned about in the general case.
Monads can help solve certain classes of problems in a uniform way. We’ve had a look at this from a syntactic perspective. Rather than complicating language designs and grammars with additional features, we embed these problems in a Monadic framework, resulting in more economic notation, that, in addition to the problems mentioned above, can be adapated to additional situations.