The essence of functional programming
2019/02/08 — Mikaël Mayer.
This article requires readers to be familiar with the notion of functional programming. It is written in Haskell. If you prefer Elm, you can also read the Elm version of the article.
Programming is an art. There are many ways to achieve the same purpose. In this article, I will informally review some of the common idioms when it comes to chaining operations on data.
Let us look at the following function in Javascript. It converts the input using three converters:
convert
might return
null
,
convert2
always succeeds, and
convert3
might return null.
convertData
returns either an error or a result:
From imperative to pattern matching
To rewrite
convertData
to a functional language, such as Haskell, we first assume the following type definitions for the functions
convert
,
convert2
and
convert3
:
Now we can rewrite
convertData
using pattern matching:
From pattern matching to monadic style
However, besides type-checking, pattern matching does not provide that much benefit. For example, we still have intermediate names.
To avoid intermediate names, we can re-write our function in a monadic style (press Next> to view the refactoring step by step):
The operator
|>
passes the computation to its left as the last argument of the expression to the right, and the operator
.>
composes functions such that the function on its left is executed first
(assuming the Flow operators for forward composition).
This formulation is useful, because now we don't need to name intermediates, we can just chain functions.
From pattern matching to continuation-passing style
There are several drawbacks to the monadic approach. First, compared to pattern matching, it reports problems at the end and in reverse order, which is not intuitive.
Second, it still needs indentation that will increase if we add conversions, which we did not need in the JavaScript imperative version.
Fortunately, there is another way to express the initial chain to catch early errors, using the maybe
operator:
Using maybe
, let's start over from pattern matching and express convertData
in another way:
It might seem that we cannot refactor further. Fortunately, we can define with
that transforms function application to callback style:
so that now we can continue the transformation:
The operator $
passes the expression at its right (in our case, always a callback) as the last argument of the expression to its left.
In general, callbacks have also the advantage of being immediately compatible with asynchronous computations.
Moreover, what if convertData
also accepted a callback as the first argument? Then we would be able to express it concisely as well.
The operator .
composes functions naturally, meaning (f . g) x
= f (g x)
.
At this point, it's normal to pause and marvel a bit. We use the operator .
which is normally used to chain functions from right to left, that is,
(f . g) x
= f (g x)
. In our case, because they are callbacks, the chaining makes the callbacks to be executed left to right, because
(f . g) callback x
=
(f $ g callback) x
=
f (g callback) x
=
f (\y -> g callback y) x
which guarantees that g is executed with the result of f, not the other way round.
Non-linear workflows
The previous style is great, but what if we need to refer to some of the intermediate results to parametrize a further computation?
Although named disappeared, it's possible to reintroduce them.
Suppose that the new signature for convert3
is the following:
Because of the callback-style workflow, it would be natural to write:
However, it is hard to see how to reduce that further, and remove the b
after parentheses.
Looking closely, we see that the callback is in any case applying b
to some function.
This means we can give a name to this construct.
Let us introduce the nameIt
operator that builds a callback by applying the value twice to it:
Now, the workflow can be rewritten:
To fully get rid of the callback
argument, we would have to make nameIt
accept it as second argument:
Thus the final function becomes:
Because . has higher precedence over $ , we cannot remove the final parentheses.
So choosing the earlier "$ " version vs. the ". " version just above is a matter of taste.
Epilogue: Towards Church encoding
It is surprising that, using default operators such as .
, we obtain a code as concise as the imperative style, with error catching in the correct order.
In his minimalistic functional language where only functions are values, Church naturally encodes datatypes Maybe
and Either
as continuation passing style.
Therefore, a Just x
becomes a (\onNothing onJust -> onJust x)
, whereas a Nothing
becomes a (\onNothing onJust -> onNothing)
.
Continuation-passing style is therefore not something new, but the ancestral way of writing functional code.
The inspiration of this article came from the build file itself I used to build this blog (here and here).
This work was supported by Swiss National Science Foundation Early Postdoc.Mobility Fellowship No. 175041