(* 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)