Even though it's already tail-recursive, It's still interesting to see a CPS version of it.
Here's the standart fold_left:
let rec myFoldLeft f acc list =
match list with
| [] -> acc
| h :: tl -> myFoldLeft f (f h acc) tl
;;
Here's an example of fold_right
let rec myFoldRightCPS f acc list cont =
match list with
| [] ->
cont acc
| h :: tl ->
myFoldRightCPS f acc tl (fun t -> cont @@ f t h)
I'm using OCaml for this one
The function
cps_fold_leftneeds to get an accumulator, a folding function, and a function to call with the result of the fold.Also, the folding functions is also in CPS style, meaning that it accepts an accumulator value, a new value, and a continuation to call to send its result.
As we will see in the implementation, you can even accept two continuations in the folding function: the continuation to call when you want to continue folding, and a continuation to call if you want to stop folding.
For example if you fold a multiplication, the result is zero as soon as you encounter a zero, there is no need to iterate over the rest of the list.
If you call the function with an accumulator that is equal to zero, you call
stop, otherwise, you callnext.If you want to write
cps_fold_left, you need to remember that you don't call the function recursively, you will call the givenfoldfunction, for examplecps_mult, with arguments that are functions to call on completion.You can test is as follows:
Lazy iterators
Having the user-provided folding function be in CPS form itself is useful (e.g. you can short-circuit iteration). Also, notice that the result of
cps_fold_leftis also the result offold, so you can return a value fromfold, and by doing so you can interrupt the iteration.Here for example I define a cursor as being either a result (R) or a cursor (C) which is an intermediate result and function of no argument that produces a next cursor.
I can write
lazy_multas follows:For now I don't care about the
stopcase, but this can be encapsulated in the cursor too. Then I can call the samecps_left_foldfunction to now have a lazy stream of values:Thanks to the following
iterfunctionI can
iter cursor:And with that, you can iterate over two lists concurrently, something that is hard to do with plain higher-order functions like
List.fold_left.Remark
Using
cps_fold_leftas define above, it is possible to write aliftfunction that takes a regular operation like( * )and turn it into a CPS style function:Then either you can change
cps_fold_leftso that it callsliftitself (and it only accept regular functions), or you let the user call it manually.The other option I see is to just call
fold acc x, get a new value, and directly callcps_fold_leftwith the same continuation. That would be identical tofold_leftexcept that you eventually call a continuation on the result, and that could be refactored ascont @@ fold_left fold acc xs. That doesn't seem useful to me, I mean there is no need to write the function in CPS style in that case.Or, as in your example, the computation could happen in a fresh continuation, but I don't see a simple way to do that without allocating data to apply the function in the right order.
In your example of
myFoldRightCPS, a new continuation is created at each level of recursion to make sure that the rightmost element is applied first to the initial accumulator value, and then the result is passed to the previous value, etc. until the function is applied to the first element.This is useful because it makes
myFoldRightCPStail-recursive, unlike the direct implementation, and then continuations calling themselves are recursive too, and also tail-recursive. So from that point of view there is an advantage in using the CPS style, which is that the call stack usage is constant w.r.t. input size. The code still allocates data for the intermediate closures, but that's expected.For the fold left operation, there is no need to delay the computation of the folding operation, and in fact once you delay it you have to reverse the order of operation to obtain the original behavior.