Sometimes, a list-processing function is partial in the sense that it is not well-defined for all of the lists. For instance, the function list0_head for returning the head element of a given list is defined only if the given list is non-empty:
fun {a:t@ype} list0_head(xs: list0(a)): a = ( case+ xs of | list0_cons(x, _) => x | _(* list0_nil() *) => $raise ListSubscriptExn() )
// datatype option0(a:t@ype) = Some0 of (a) | None0 of () // fun {a:t@ype} list0_head_opt (xs: list0(a)): option0(a) = ( case+ xs of list0_cons(x, _) => Some0(x) | _ => None0() )
// fun {a:t@ype} list0_last_opt ( xs: list0(a) ) : option0(a) = let // fun loop (x0: a, xs: list0(a)): a = ( case+ xs of | list0_nil() => x0 | list0_cons(x1, xs) => loop(x1, xs) ) // in // case+ xs of | list0_nil() => None0() | list0_cons(x, xs) => Some0(loop(x, xs)) // end // end of [list0_last_opt] //
Before moving on, I would like to point out a very common mistake in (functional) list-processing. First and foremost, a list is not meant to be used like an array. The following (partial) function list0_get_at does the so-called list-subscripting:
// extern fun {a:t@ype} list0_get_at (xs: list0(a), n: int): a // implement {a}(*tmp*) list0_get_at (xs, n) = ( case+ xs of | list0_nil() => $raise ListSubscriptExn() | list0_cons(x, xs) => if n <= 0 then x else list0_get_at<a>(xs, n-1) ) //
// fun list0_tally1 (xs: list0(int)): int = list0_foldleft<int><int>(xs, 0, lam(res, x) => res + x) // fun list0_tally2 (xs: list0(int)): int = int_foldleft<int> (list0_length(xs), 0, lam(res, i) => res + list0_get_at<int>(xs, i)) //
Let us see more functions for performing functional list-processing in the following presentation and an example of functional programming at the end that illustrates some typical use of list-processing functions.
A commonly used (higher-order) function is often referred to as list-map, which takes a list and a function and returns a newly constructed list consisting of all of the elements obtained from applying the given function to each element in the given list:
// extern fun {a:t@ype} {b:t@ype} list0_map (xs: list0(a), fopr: cfun(a, b)): list0(b) // implement {a}{b} list0_map ( xs, fopr ) = auxlst(xs) where { // fun auxlst (xs: list0(a)): list0(b) = ( case+ xs of | list0_nil() => list0_nil() | list0_cons(x, xs) => list0_cons(fopr(x), auxlst(xs)) ) // } (* end of [list0_map] *) //
With list0_map, we can readily build list0_cross as follows for computing the cross product of two given lists:
// extern fun {a,b:t@ype} list0_cross (xs: list0(a), ys: list0(b)): list0($tup(a, b)) // implement {a,b}(*tmp*) list0_cross (xs, ys) = let // typedef ab = $tup(a, b) // in // list0_concat // for concatenating a list of lists ( list0_map<a><list0(ab)> (xs, lam(x) => list0_map<b><ab>(ys, lam(y) => $tup(x, y))) ) (* end of [list0_concat] *) // end // end of [list0_cross] //
A function rather similar to list0_map is list0_foreach, which takes a list and a procedure (i.e., a function returning void) and applies the procedure to each element in the list (so as to create some effects):
// extern fun {a:t@ype} list0_foreach (xs: list0(a), fwork: cfun(a, void)): void // implement {a}(*tmp*) list0_foreach ( xs, fwork ) = loop(xs) where { // fun loop (xs: list0(a)): void = ( case+ xs of | list0_nil() => () | list0_cons(x, xs) => (fwork(x); loop(xs)) ) // } (* end of [list0_foreach] *) //
Another commonly used (higher-order) function is often referred to as list-filter, which takes a list and a predicate (i.e., a function returning a boolean value) and returns a newly constructed list consisting of all of the elements in the given list that satisfy the given predicate:
// extern fun {a:t@ype} list0_filter (xs: list0(a), test: cfun(a, bool)): list0(a) // implement {a}(*tmp*) list0_filter ( xs, test ) = auxlst(xs) where { // fun auxlst (xs: list0(a)): list0(a) = ( case+ xs of | list0_nil() => list0_nil() | list0_cons(x, xs) => if test(x) then list0_cons(x, auxlst(xs)) else auxlst(xs) // end of [if] ) // } (* end of [list0_filter] *) //
Given a list xs, the following function list0_remdup removes all of the elements in xs that have already appeared previously:
// extern fun {a:t@ype} list0_remdup (xs: list0(a), eqfn: cfun(a, a, bool)): list0(a) // implement {a}(*tmp*) list0_remdup(xs, eqfn) = ( case+ xs of | list0_nil() => list0_nil() | list0_cons(x0, xs) => list0_cons(x0, list0_remdup<a>(list0_filter<a>(xs, lam(x) => ~eqfn(x0, x)), eqfn)) ) //
The list-processing function list0_map processes every element in its list argument and so does list0_filter. Sometimes, we need a list-processing function that stops immediately after certain condition is met. For instance, we may want to locate the index of the first element in a given list that satisfies some test, which can be done by calling the following function list0_find_index:
// extern fun {a:t@ype} list0_find_index (xs: list0(a), test: cfun(a, bool)): int // implement {a}(*tmp*) list0_find_index (xs, test) = let // fun loop (xs: list0(a), i: int): int = ( case+ xs of | list0_nil() => ~1 | list0_cons(x, xs) => if test(x) then i else loop(xs, i+1) ) // in loop(xs, 0) end // end of [list0_find_index] //
Given a list0-value xs and a predicate test, list0_exist returns true if and only if there exists one element in xs satisfing test, and list0_forall returns true if and only if every element in xs satisfies test. Both of these two functions can be readily implemented based on a direct call to list0_find_index:
// extern fun {a:t@ype} list0_exists (xs: list0(a), test: cfun(a, bool)): bool extern fun {a:t@ype} list0_forall (xs: list0(a), test: cfun(a, bool)): bool // implement {a}(*tmp*) list0_exists(xs, test) = list0_find_index<a>(xs, test) >= 0 implement {a}(*tmp*) list0_forall(xs, test) = list0_find_index<a>(xs, lam(x) => not(test(x))) < 0 //
Given a list (x0, ..., xn-1) of length n, its indexed version refers to the list of pairs in which the elements are of the form (i, xi) for i ranging from 0 to n-1. For a function that processes a given list, there is often a meaningful variant of the function that processes the indexed version of the list. For instance, the following function list0_imap is such a variant of list0_map:
// extern fun {a:t@ype} {b:t@ype} list0_imap (xs: list0(a), fopr: cfun(int, a, b)): list0(b) // implement {a}{b} list0_imap ( xs, fopr ) = auxlst(0, xs) where { // fun auxlst (i: int, xs: list0(a)): list0(b) = ( case+ xs of | list0_nil() => list0_nil() | list0_cons(x, xs) => list0_cons(fopr(i, x), auxlst(i+1, xs)) ) // } (* end of [list0_imap] *) //
I have so far presented a variety of functions for processing lists in a functional style. I would like to conclude the chapter with an example of functional programming that can concretely demonstrate some typical use of list-processing functions in practice.
The famous 8-queen puzzle asks the player to find ways to put eight queen pieces on a chess board such that no queen piece can attack any other ones. In other words, no two queen pieces can be put on the same row, the same column, or the same diagnal. This puzzle can be readily solved with a tree-based search. Let us introduce an abstract type as follows:
where ptr indicates that node is boxed. Intuitively, a node represents a partial solution where there are n queen pieces (for some n less than or equal to 8) on the first n rows of the chess board such that no one piece can attack any other pieces. Given a node, another node extending it with one more queen piece is considered its child. The following declared function node_get_children is supposed to be called to obtain all of the child nodes of a given node: With node_get_children, we can readily implement node_dfsenum as follows for enumerating in the depth-first manner all of the nodes contained in the tree rooted at a given node:// extern fun node_dfsenum(node): list0(node) // implement node_dfsenum (nx0) = list0_cons ( nx0 , list0_concat<node> ( list0_map<node><list0(node)>(nx0.children(), lam(nx) => node_dfsenum(nx)) ) (* list0_concat *) ) (* node_dfsenum *) //
// extern fun QueenSolve(): list0(node) // #define N 8 // implement QueenSolve() = list0_filter<node>(node_dfsenum(node_init()), lam(nx) => node_length(nx) = N) //
// assume node = list0(int) // implement node_init() = list0_nil() // implement node_length(nx) = list0_length(nx) //
// fun test_safety ( xs: list0(int) ) : bool = let // val- list0_cons(x0, xs) = xs // in // list0_iforall<int> // abs: absolute value (xs, lam(i, x) => (x0 != x && abs(x0-x) != (i+1))) // end // end of [test_safety] // implement node_get_children (nx) = list0_filter<node> (int_list0_map<node> (N, lam(x) => list0_cons(x, nx)), lam(nx) => test_safety(nx) ) //
The presented code for solving 8-queen puzzle is of the kind of high-level functional programming style I intend to advocate throughout the book. It should soon be clear that we can reap even more benefits from programming in this way when (linear) lazy streams are used in place of functional lists.
Please find on-line the entirety of the code used in this chapter.