Factorial can be implemented in Elm as follows:
fact : Int -> Int fact n = if n <= 1 then 1 else n * fact (n - 1)
The JavaScript produced by this implementation looks like:
var $author$project$Main$fact = function (n) { return (n <= 1) ? 1 : (n * $author$project$Main$fact(n - 1)); };
So the recursive Elm implementation produced a recursive JavaScript implementation. In the terminology of SICP: Procedures and the Processes They Generate, fact
can be described as a recursive function that generates a recursive process.
Experienced functional programmers know we can write the following version of the factorial function:
factIter : Int -> Int factIter n = factIterHelper n 1 factIterHelper : Int -> Int -> Int factIterHelper n acc = if n <= 1 then acc else factIterHelper (n - 1) (acc * n)
And the JavaScript produced by this implementation will produce a while loop if the language implements tail-call optimization, which Elm does.
var $author$project$Main$factIterHelper = F2( function (n, acc) { factIterHelper: while (true) { if (n <= 1) { return acc; } else { var $temp$n = n - 1, $temp$acc = acc * n; n = $temp$n; acc = $temp$acc; continue factIterHelper; } } }); var $author$project$Main$factIter = function (n) { return A2($author$project$Main$factIterHelper, n, 1); };
As a result, factIter
is a recursive function that generates an iterative process. This is much better since looping tends to be faster than making function calls.
Anyway, what I wanted to share today was something I recently learned about continuation-passing style and factIter
.
Continuation-passing style (CPS)
Continuation-passing style (CPS) is a style of programming which allows you to transform functions such that all function calls end up as tail calls.
We do this by adding an extra argument to each function. The extra argument is called a continuation. The function you're transforming returns by "calling the continuation". In Elm, the continuation can be implemented using custom types or functions.
Factorial using CPS (custom types)
fact : Int -> Int fact n = factK n endCont factK : Int -> Continuation -> Int factK n k = if n <= 1 then applyContinuation k 1 else factK (n - 1) (factNCont n k) type Continuation = EndCont | FactNCont Int Continuation endCont : Continuation endCont = EndCont factNCont : Int -> Continuation -> Continuation factNCont = FactNCont applyContinuation : Continuation -> Int -> Int applyContinuation k val = case k of EndCont -> val FactNCont n nextK -> applyContinuation nextK (n * val)
Factorial using CPS (functions)
fact : Int -> Int fact n = factK n endCont factK : Int -> Continuation -> Int factK n k = if n <= 1 then applyContinuation k 1 else factK (n - 1) (factNCont n k) type alias Continuation = Int -> Int endCont : Continuation endCont = \val -> val factNCont : Int -> Continuation -> Continuation factNCont n nextK = \val -> applyContinuation nextK (n * val) applyContinuation : Continuation -> Int -> Int applyContinuation k val = k val
Towards a clever representation of continuations
Sometimes we can find clever representations of continuations. If we notice the following about the previous representation:
-
endCont
multiplies its value by1
, becauseendCont = \val -> 1 * val
. - If
nextK
multiplies its value bym
thenfactNCont n nextK
multiples its value bym * n
.
Then, we will see that both continuations are of the form \val -> m * val
for some m
. It means we can represent such a continuation by the multiplier m
. Doing that we get:
fact : Int -> Int fact n = factK n endCont factK : Int -> Continuation -> Int factK n k = if n <= 1 then applyContinuation k 1 else factK (n - 1) (factNCont n k) type alias Continuation = Int endCont : Continuation endCont = 1 factNCont : Int -> Continuation -> Continuation factNCont n nextK = applyContinuation nextK n applyContinuation : Continuation -> Int -> Int applyContinuation k val = k * val
Finally, if we inline endCont
, factNCont
, and applyContinuation
and get rid of the Continuation
type alias, we end up with the following implementation:
fact : Int -> Int fact n = -- factK n endCont -- = factK n 1 factK : Int -> Int -> Int factK n k = if n <= 1 then -- applyContinuation k 1 -- = k * 1 -- = k else -- factK (n - 1) (factNCont n k) -- = factK (n - 1) (applyContinuation k n) -- = factK (n - 1) (k * n)
Isn't that cool? It's equivalent to the factIter
we started off with.
An accumulating parameter is often just a representation of a continuation.
If this blew your mind and piqued your interests then I'd highly recommend you to read section 6.1 Writing Programs in Continuation-Passing Style from the book Essentials of Programming Languages, 3rd Edition.
Top comments (1)
I finally understand "continuation passing" - this is outstanding.
Seriously - I have tried to understand this for many, many years; and it turns out that I have understood it, in a sense; I've been advocating for defunctionalization everywhere that it fits for a minute now, but this is another level.
Appreciate it.