I am struggling with understanding foldback with CPS. This one I can understand:
let listFoldBack combine acc l =
let rec Loop l cont =
match l with
| h :: t -> Loop t (fun racc -> cont (combine h racc))
| [] -> cont acc
Loop l id
listFoldBack (fun x a -> (2*x::a) ) [] [1;2;3]
// call sequence
[1;2;3] id
Loop [2;3] (fun a -> (combine 1 a))
Loop [3] (fun a -> (combine 1 (combine 2 a)))
Loop [] (fun a -> (combine 1 (combine 2 (combine 3 a))))
(fun a -> (combine 1 (combine 2 (combine 3 a)))) []
2::(4::(6::[]))
But then:
let fib n =
let rec fibc a cont =
if a <= 2 then cont 1
else
fibc (a - 2) (fun x -> fibc (a - 1) (fun y -> cont(x + y)))
fibc n id
// call sequence
fibc (4) id
fibc (2) (fun x -> fibc (3) (fun y -> id(x + y)))
(fun x -> fibc (3) (fun y -> id(x + y))) (1)
fibc (3) (fun y -> id(1 + y))
fibc (1) (fun x -> fibc (2) (fun y -> (fun z -> id(1+z)) (x + y)))
fibc (2) (fun y -> (fun z -> id(1+z))(1 + y)))
(fun y -> (fun z -> id(1+z))(1 + y))) (1)
fun z -> id(1+z)(1+1)
id (1+2)
3
Very difficult to follow.
Even worse:
type Tree<'a> =
| Leaf
| Node of 'a * Tree<'a> * Tree<'a>
let node x l r = Node(x,l,r)
let treeFoldBack f leaf tree =
let rec loop t cont =
match t with
| Leaf -> cont leaf
| Node(x,l,r) -> loop l (fun lacc ->
loop r (fun racc -> cont(f x lacc racc)))
loop tree id
let tree1 =
(node 4
(node 2
(node 1 Leaf Leaf)
(node 3 Leaf Leaf))
(node 6
(node 5 Leaf Leaf)
(node 7 Leaf Leaf)))
// call sequence by means of text replacing
... a lot of steps, lines too long to fit
cont(f 4 (f 2 (f 1 Leaf Leaf) (f 3 Leaf Leaf))
(f 6 (f 5 Leaf Leaf) (f 7 Leaf Leaf))))
The result is correct, but very difficult to follow. For all cases the pattern is like:
loop l (fun lacc -> loop r (fun racc -> cont(f x lacc racc)))
calculate loop l, result goes in lacc.
calculate loop r, result goes in racc.
calculate f x lacc racc and result is argument for cont.
Why does this work? how to understand this?
I think that the best way to understand continuation passing style is to compare the ordinary non-continuation version of a function with the continuation-based function. Using your "even worse" example of tree fold, let's write the function using ordinary recursion first:
let treeFoldBack f leaf tree =
let rec loop t =
match t with
| Leaf -> leaf
| Node(x,l,r) ->
let lacc = loop l // Process left and return result
let racc = loop r // Process right and return result
f x lacc racc // Aggregate current, left and right
loop tree
As you can see, the function is not tail-recursive in the Node
case, because we call loop
, then call loop
again and then finally call f
.
The idea of continuations is to add a parameter that represents "what to do after the current operation has completed". This is captured by cont
. In case of Leaf
, we just replace returning leaf
with calling continuation with leaf
as an argument. The Node
case is more elaborate:
let treeFoldBack f leaf tree =
let rec loop t cont =
match t with
| Leaf -> cont leaf
| Node(x,l,r) ->
loop l (fun lacc -> // Process left and continue with result
loop r (fun racc -> // Process right and continue with result
cont(f x lacc racc))) // Aggregate and invoke final continuation
loop tree id
As you can see, the structure is the same as before - but rather than calling loop
and storing the result using let
, we now call loop
and pass it a function that gets the result and does the rest of the work.
This looks a bit confusing until you get used to it - but the key thing is that this is actually a fairly mechanical translation - you just replace let
with fun
in the right way!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With