A list-folding combinator is a (higher-order) function that traverses a given list to process the elements contained in the list. In this very light article (based on a lecture of mine on introductory functional programming), I present some code implementing 3 list-folding combinators as well as several examples making use of these combinators.
The function list_fold_left of the following interface folds a given list from left to right:
fun {a,b:t@ype} list_fold_left ( xs: List(a) , init: b, fopr: (b, a) -<cloref1> b ) : b // end-of-functionApplied to a list of n elements (x1, ..., xn), list_fold_left essentially returns the value of the following (informal) expression: fopr(...fopr(init, x1), ..., xn). The following code gives a typical implementation of list_fold_left:
implement {a,b}(*tmp*) list_fold_left (xs, init, fopr) = let // fun auxmain {n:nat} .<n>. ( init: b, xs: list(a, n) ) : b = ( // case+ xs of | list_nil() => init | list_cons(x, xs) => auxmain(fopr(init, x), xs) // end of [list_cons] // ) (* auxmain *) // prval () = lemma_list_param(xs) // in auxmain(init, xs) end // end of [list_fold_left]Note that auxmain is tail-recursive.
As an example, the list-length function can be implemented with a direct call to list_fold_left:
fun {a:t@ype} list_length ( xs: List(a) ) : int = ( // list_fold_left<a,int> (xs, 0, lam(xs, x) => xs + 1) // ) (* list_length *)However, it should be noted that this implementation of list_length cannot be assigned the following interface:
fun
{a:t@ype}
list_length{n:nat}(list(a, n)): int(n)
In order to do so, a more accurate interface needs to be given to
list_fold_left.
fun {a:t@ype} list_reverse ( xs: List(a) ) : List0(a) = ( // list_fold_left<a,List0(a)> (xs, list_nil(), lam(xs, x) => list_cons(x, xs)) // ) (* list_reverse *)Note that the type constructor List0 is defined as follows:
typedef List0(a:t@ype) = [n:nat] List(a)
The function list_fold_right of the following interface folds a given list from right to left:
fun {a,b:t@ype} list_fold_right ( xs: List(a) , fopr: (a, b) -<cloref1> b, sink: b ) : b // end-of-functionApplied to a list of n elements (x1, ..., xn), list_fold_right essentially returns the value of the followng (informal) expression: fopr(x1,...fopr(xn, sink)...). The following code gives a typical implementation of list_fold_right:
implement {a,b}(*tmp*) list_fold_right (xs, fopr, sink) = let // fun auxmain {n:nat} .<n>. ( xs: list(a, n) ) : b = ( // case+ xs of | list_nil() => sink | list_cons(x, xs) => fopr(x, auxmain(xs)) // ) (* auxmain *) // prval () = lemma_list_param(xs) // in auxmain(xs) end // end of [list_fold_right]Note that auxmain is recursive but not tail-recursive.
As an example, the list-length function can be implemented with a direct call to list_fold_right:
fun {a:t@ype} list_length ( xs: List(a) ) : int = ( // list_fold_right<a,int> (xs, lam(x, xs) => xs + 1, 0) // ) (* list_length *)Compared to the implementation of list_length based on list_fold_left, this one is much less attractive as it is not tail-recursive and thus may cause a call-stack overflow when applied to a long list (for instance, one containing 1 million elements).
fun {a:t@ype} list_append ( xs: List(a), ys: List(a) ) : List0(a) = let // prval () = lemma_list_param(ys) // in // list_fold_right<a,List0(a)> (xs, lam(x, xs) => list_cons(x, xs), ys) // end (* list_append *)In practice, it is not uncommon to see an implementation of the list-reverse function (for reversing a given list) of the following style (usually by a novice functional programmer):
fun {a:t@ype} list_reverse ( xs: List(a) ) : List0(a) = ( // list_fold_right<a,List0(a)> ( xs , lam(x, xs) => list_append<a>(xs, list_cons(x, list_nil())) , list_nil((*void*)) ) (* end of [list_fold_right] *) // ) (* list_reverse *)This is a terribly inefficient implementation of O(n2)-complexity both time-wise and memory-wise, and any programmer who writes this kind of code is surely in need of solidifying his/her own understanding of the call-by-value semantics.
The following code implements based on list_fold_right a function searching for the rightmost element satisfying a given prediate in a given list:
fun {a:t@ype} list_find_rightmost ( xs: List(a), p: (a) -<cloref1> bool ) : Option(a) = let // exception Found of (a) // in // try let // val _ = list_fold_right<a,int> ( xs , lam(x, xs) => if p(x) then $raise(Found(x)) else (0) , 0(*nominal*) ) // in None((*void*)) end with ~Found(x) => Some(x) // end (* end of [list_find_rightmost] *)Note that the fopr argument passed to list_fold_right in this case is a function that raises an exception carrying the element being processed if the element satsifies the given predicate. As the type void may cause difficulty for the generated C code to be compiled properly, the type int is chosen instead for the result returned by the call to list_fold_right.
Lists are inherently sequential, and traversing a list is mostly likely done in a from-left-to-right or from-right-to-left manner. There are however realistic occasions where one may want to traverse a list in a tree-like manner. For instance, list-mergesort can be implemented by first splitting a given list into two halves and then recursively sorting them and merging the two obtained sorted lists into one. The following function list_fold_split can be seen as a form of abtraction over mergesort:
fun {a,b:t@ype} list_fold_split ( xs: List(a) , fopr: (b, b) -<cloref1> b , sink0: b, fsink1: (a) -<cloref1> b ) : b // end-of-functionIf a given list contains at least two elements, list_fold_split splits it into two halves and then applies recursively to them to yield two results that are subsequently combined together by a call to fopr; if the given list is empty, then sink0 is the result; if the given list is a singleton, the fsink1 is called on the only element to yield the result. The following code gives an implementation of list_fold_split:
implement {a,b}(*tmp*) list_fold_split (xs, fopr, sink0, fsink1) = let // fun aux { n1,n2:nat | n1 >= n2 } .<n2>. ( xs: list(a, n1), n2: int(n2) ) : b = ( if (n2 >= 2) then let val n21 = half(n2) in fopr(aux(xs, n21), aux(list_drop(xs, n21), n2-n21)) end // end of [then] else ( // case+ xs of | list_nil() => sink0 | list_cons(x, _) => fsink1(x) // ) (* end of [else] *) // ) (* end of [aux] *) // prval () = lemma_list_param(xs) // in aux(xs, length(xs)) end // end of [list_fold_split]Given a list and a natural number n less than or equal to the length of the list, list_drop returns another list obtained from removing the first n elements from the given list. Clearly, list_drop is O(n)-time. Assume that fsink1 is O(1)-time. If fopr is O(1)-time, then list_fold_split is O(n(log(n)))-time. If fopr is O(n)-time, then list_fold_split is O(n(log(n)))-time as well.
Unsuprisingly, list-mergesort can be implemented with a direct call to list_fold_split where the fopr argument is the standard function for merging two sorted lists into one:
local // fun{ a:t@ype } merge ( xs0: list0(a) , ys0: list0(a) ) : list0(a) = ( case+ (xs0, ys0) of | (list0_nil(), _) => ys0 | (_, list0_nil()) => xs0 | (list0_cons(x0, xs1), list0_cons(y0, ys1)) => let val sgn = gcompare_val_val<a>(x0, y0) in if sgn <= 0 then list0_cons(x0, merge(xs1, ys0)) else list0_cons(y0, merge(xs0, ys1)) end // end of [cons _, cons _] ) // in (* in-of-local *) // fun {a:t@ype} mergesort(xs: List(a)) = list_fold_split<a,list0(a)> ( xs , lam(xs, ys) => merge<a>(xs, ys) , list0_nil(), lam(x) => list0_sing(x) ) // end // end of [local]As another example, the list-reverse function can be implemented as follows:
fun {a:t@ype} list_reverse ( xs: List(a) ) : List0(a) = ( // list_fold_split<a,List0(a)> ( xs , lam(xs, ys) => list_append(ys, xs) , list_nil, lam(x) => list_cons(x, list_nil) ) (* list_fold_split *) // ) (* list_reverse *)Note that this implementation of list_reverse is O(n(log(n)))-time. While it is better than the previous one of O(n2)-time, it is still unacceptably poor as reversing a list can be readily given a tail-recursive implementation of O(n)-time.
Please find in the following files the entirety of the code presented in this article:
list_fold_left.dats list_fold_right.dats list_fold_split.datsIn addition, there is an accompanying Makefile for compiling and testing the code.
This article is written by Hongwei Xi.