We've talked about different implementations of lists in the Lambda Calculus; and we've also talked about using lists to implement other data structures, like sets. One thing we observed is that if you're going to implement a set using a list (this isn't an especially efficient implementation of a set, but let's do the best we can with it), it's helpful to make sure that the list is always sorted. That way, as you're searching through the list, you might come to a point before the end where you knew the element you're looking for wasn't going to be found anymore. So you wouldn't have to continue the search.

If you were implementing lists with letrec or a fixed point combinator, then at that point you could just have your traversal function return some appropriate result, instead of requesting that the recursion continue.

But what if you were using our right-fold or left-fold implementation of lists, rather than using letrec or a fixed point combinator? It's true that letrec and fixed point combinators are cool. But if you use them, the terms you construct won't be strongly normalizing. Whether their reduction stops at a normal form will depend on what evaluation order is operative. For efficiency, it can also be helpful to know how to get by without those powerful devices when they're not strictly necessary. Plus that knowledge could carry over to settings where letrec is no longer available, even in principle.

When dealing with a right-fold or left-fold implementation of lists, if your traversal function returns a result, that result automatically gets passed to the next stage of the traversal, as the updated seed value from the previous steps. If we make the seed value type complex, we could signal to the rest of the traversal that the job is done, they don't need to do any more work. For example, the seed value might be a pair of false and some default value until we reach a certain stage, and all the traversal steps until that stage have to do their normal work, but once we've gotten to the stage where we're ready to return a result, we make the seed value be a pair of true and the result we've computed. Then all the later traversal steps see that true and just pass the existing seed pair on to the rest of the traversal. At the end, we throw away the true and take the second member of the final seed value as our desired result.

That will work OK, but we still have to go through every step of the traversal. Let's think about whether we can modify the right-fold and/or left-fold implementation of lists to allow for a genuine early abort. We'd like to avoid any unnecessary steps of the traversal. If we've worked out our result mid-way through the traversal, we want to be able to deliver it immediately to the larger computation in which our traversal was embedded. The problem with the fold-based implementations of lists we've got so far is that they are pre-programmed to traverse the whole list. We'd have to implement the folds differently to be able to achieve what we're now envisaging.

We worked out such an implementation in the homework session on Wed April 22. The scheme we used was that, whereas before our traversal functions would have an interface like this:

\current_list_element seed_value_so_far. do_something


Our traversal functions will instead now have an interface like this:

\current_list_element seed_value_so_far done_handler keep_going_handler.
if ... then done_handler (some_result) else keep_going_handler (another_result)


and we worked out that a left-fold implementation of the list [10,20,30,40] could look like this:

\f z done_handler. f 10 z done_handler (\z. [20,30,40] f z done_handler)


This is all reminiscent of the way in which our encodings of triples and pairs and such too their handler functions as arguments. Remember with triples, we say:

triple (\x y z. add x y)


or:

triple (\x y z. z)


to get the last element of the triple. Of course you can wrap this up in another function, that takes the triple as its argument, but at bottom, you are going to be supplying the handler to the triple. And then the handler gets the triple's elements, not the triple itself, as arguments. (Another of our implementations of lists followed a similar strategy.)

Something similar is happening here, where both the traversal function f and the fold function that implements the list both take such handler arguments. The only not-completely-straightforward thing going on in the fold function is that the keep_going_handler we pass to the traversal function f encodes how the fold should continue, using the updated seed value z from the current step, without actually computing the rest of the traversal. It's up to the body of f to decide whether to invoke the rest of the traversal, by supplying a value to that keep_going_handler, or to finish the traversal right now by supply a value to the done_handler instead.

A question came up in the session of why we need the done_handler in this scheme. We could just eliminate it and have the traversal function f choose between simply returning a value --- that'd abort the traversal, the way we did above by passing the value to done_handler --- or instead supplying a value to the keep_going_handler. And the answer is that yes, in this particular scheme, that is correct. (In a few weeks when we look at delimited continuations in terms of reset/shift, you'll see that what we just described is how the abort operation gets implemented in terms of shift.) However, sometimes it helps to express a basic case a bit more verbosely than seems immediately necessary, because then later generalizations will look more natural. That's true here. So let's just use the implementation as we've written it. If you prefer, you can just make the done_handler be the identity function.

With that general scheme, here is the empty list:

\f z done_handler. done_handler z


and here is the cons operation:

\x xs. \f z done_handler. f x z done_handler (\z. xs f z done_handler)


(The latter can just be read off of our construction of [10,20,30,40]; I just substituted x for 10 and xs for [20, 30, 40].)

Here's an example of how to get the head of such a list:

xs (\x z done keep_going. done x) err done_handler


err is what's returned if you ask for the head of the empty list.

Here's how to get the length of such a list:

xs (\x z done keep_going. keep_going (succ z)) 0 done_handler


Here there is no opportunity to abort early with a correct value, so our traversal function always delivers its output to the keep_going handler.

Here's how to get the last element of such a list:

xs (\x z done keep_going. keep_going x) err done_handler


This is similar to getting the first element, except that each step delivers its output to the keep_going handler rather than to the done handler. That ensures that we will only get the output of the last step, when the traversal function is applied to the last member of the list. If the list is empty, then we'll get the err value, just as with the function that tries to extract the list's head.

One thing to note is that there are limits to how much you can immunize yourself against doing unnecessary work. A demon evaluator who got to custom-pick the evaluation order (including doing reductions underneath lambdas when he wanted to) could ensure that lots of unnecessary computations got performed, despite your best efforts. We don't yet have any way to prevent that. (Later we will see some ways to computationally force the evaluation order we prefer. Of course, in any real computing environment you'll mostly know what evaluation order you're dealing with, and you can mostly program efficiently around that.) The current scheme at least makes our result not computationally depend on what happens further on in the traversal, once we've passed a result to the done_handler. We don't even rely on the later steps in the traversal cooperating to pass our result through.

All of that gave us a left-fold implementation of lists. (Perhaps if you were aiming for a left-fold implementation of lists, you would make the traversal function f take its current_list_element and seed_value arguments in the flipped order, but let's not worry about that.) Now, let's think about how to get a right-fold implementation. It's not profoundly different, but it does require us to change our interface a little. Our left-fold implementation of [10,20,30,40], above, looked like this (now we abbreviate some of the variables):

\f z d. f 10 z d (\z. [20,30,40] f z d)


Expanding the definition of [20,30,40], and all the successive tails, this comes to:

\f z d. f 10 z d (\z. f 20 z d (\z. f 30 z d (\z. f 40 z d d)))


For a right-fold implementation, that should instead look like roughly like this:

\f z d. f 40 z d (\z. f 30 z d (\z. f 20 z d (\z. f 10 z d d)))


Now suppose we had just the implementation of the tail of the list, [20,30,40], that is:

\f z d. f 40 z d (\z. f 30 z d (\z. f 20 z d d))


How should we take that value and transform it into the preceding value, which represents 10 consed onto that tail? I can't see how to do it in a general way, and I expect it's just not possible. Essentially what we want is to take that second d in the innermost function \z. f 20 z d d, we want to replace that second d with something like (\z. f 10 z d d). But how can we replace just the second d without also replacing the first d, and indeed all the other bound occurrences of d in the expansion of [20,30,40].

The difficulty here is that our traversal function f expects two handlers, but we are only giving a single handler to the fold function we implement the list as. That single handler gets fed twice to the traversal function. One time it may be transformed, but at the end of the traversal, as with \z. f 20 z d d, there's nothing left to do to "keep going", so here it's just the single handler d fed to f twice. But we can see that in order to implement cons for a right-folding traversal, we don't want it to be the single handler d fed to f twice. It'd work better if we implemented [20,30,40] like this:

\f z d g. f 40 z d (\z. f 30 z d (\z. f 20 z d g))


Notice that now the fold function we implement the list as takes two handlers, d (for "done") and g (for "keep going"). Generally we'll invoke the fold function by supplying the same handler function to both of these. However, it's still useful to have the list be defined so that they're separate arguments. For now we can cons 10 onto the list by just substituting (\z. f 10 z d g) in for the bound g. That is:

[10,20,30,40] ≡ \f z d g. [20,30,40] f z d (\z. f 10 z d g)


Spelling this out, here are the implementations of the functions we defined before, only now for the right-fold lists:

null = \f z d g. g z
cons x xs = \f z d g. xs f z d (\z. f x z d g)
head xs = xs (\x z d g. g x) err done_handler done_handler
length xs = xs (\x z d g. g (succ z)) 0 done_handler done_handler
last xs = xs (\x z d g. d x) err done_handler done_handler


Exercise: when considering just the implementation of null, both \f z d g. g z and \f z d g. d z may seem like reasonable candidates. What would go wrong with the rest of our scheme is we had instead used the latter?

To extract tails efficiently, too, it'd be nice to merge the apparatus developed above with the ideas from another of our implementations of lists, where we passed the traversal function f not merely the current element and result of traversing the list's tail (or in the present case, a modified keep_going handler which encodes how to do that), but also the list's tail itself. That would make some computations more efficient. But we leave this as an exercise.

Of course, like everything elegant and exciting in this seminar, Oleg discusses it in much more detail.