(* Dan Grossman; Graduate Programming Languages; Lecture 13 *)
(* CPS and tail-calls *)
(* disclaimer: code has not been tested *)
(* Assume you are in a setting where very deep call-stacks lead to run-time
errors, so you don't want recursion proportional to data-structure depth
since data structures might be large *)
(* list example: could cause stack overflow for large list *)
let rec length1 lst =
match lst with
[] -> 0
| _::tl -> 1 + length1 tl
(* list example: by using an /accumulator/, the recursive call is a /tail call/
so functional-language implementations ensure O(1) stack depth
-- a common pattern, using a helper function to hide from callers
*)
let length2 lst =
let rec f acc lst =
match lst with
[] -> acc
| _::tl -> f (acc+1) tl
in f 0 lst
(* tree example: if the tree is /balanced/, no problem, but if imbalance is
possible, this could cause stack overflow *)
type 'a tree = Null | Node of 'a * 'a tree * 'a tree
let rec size1 tree =
match tree with
Null -> 0
| Node(_,l,r) -> 1 + size1 l + size1 r
(* tree example: we can try to use an accumulator, but it won't suffice because
only one of the two recursive calls can be in tail position *)
let size2 tree =
let rec f acc tree =
match tree with
Null -> acc
| Node(_,l,r) -> f (f acc l) r
in f 0 tree
(* tree example: a tree traversal /requires/ a stack, but we can use an
explicit heap-allocated stack instead of the meta-language's call-stack *)
let size3 tree =
let rec f acc stack tree =
match tree with
Null -> (match stack with
[] -> acc
| hd::tl -> f acc tl hd)
| Node(_,l,r) -> f (acc+1) (r::stack) l
in f 0 [] tree
(* tree example: a rather slick trick is to use CPS where the heap-allocated
stack is in function closures rather than a linked list -- as always,
CPS naturally uses only tail calls *)
let size4 tree =
let rec f tree k = (* f : 'a tree -> (int -> 'b) -> 'b *)
match tree with
Null -> k 0
| Node(_,l,r) -> f l (fun i -> f r (fun j -> k (i+j+1)))
in f tree (fun x -> x)
(* tree example: while size4 uses only tail calls, it allocates two
closures for each node. Re-introducing an accumulator cuts this in half --
an accumulator needs no heap allocation *)
let size5 tree =
let rec f tree acc k =
match tree with
Null -> k acc
| Node(_,l,r) -> f l acc (fun i -> f r (1+acc) k)
in f tree 0 (fun x -> x)