~~>
......... true ~~>
?
Of course, whether those reductions actually followed in that order would
depend on what reduction strategy was in place. But the result of folding the
search function over the part of the list whose head is `3` and whose tail is `[2;
1]` will *semantically* depend on the result of applying that function to the
more rightmost pieces of the list, too, regardless of what order the reduction
is computed by. Conceptually, it will be easiest if we think of the reduction
happening in the order displayed above.
Once we've found a match between our sought number `3` and some member of
the list, we'd like to avoid any further unnecessary computations and just
deliver the answer `true` as "quickly" or directly as possible to the larger
computation in which the search was embedded.
With a Y-combinator based search, as we said, we could do this by just not
following a recursion branch.
But with the v3 lists, the fold is "pre-programmed" to continue over the whole
list. There is no way for us to bail out of applying the search function to the
parts of the list that have head `4` and head `5`, too.
We *can* avoid *some* unneccessary computation. The search function can detect
that the result we've accumulated so far during the fold is now `true`, so we
don't need to bother comparing `4` or `5` to `3` for equality. That will simplify the
computation to some degree, since as we said, numerical comparison in the
system we're working in is moderately expensive.
However, we're still going to have to traverse the remainder of the list. That
`true` result will have to be passed along all the way to the leftmost head of
the list. Only then can we deliver it to the larger computation in which the
search was embedded.
It would be better if there were some way to "abort" the list traversal. If,
having found the element we're looking for (or having determined that the
element isn't going to be found), we could just immediately stop traversing the
list with our answer. **Continuations** will turn out to let us do that.
We won't try yet to fully exploit the terrible power of continuations. But
there's a way that we can gain their benefits here locally, without yet having
a fully general machinery or understanding of what's going on.
The key is to recall how our implementations of booleans and pairs worked.
Remember that with pairs, we supply the pair "handler" to the pair as *an
argument*, rather than the other way around:
pair (\x y. add x y)
or:
pair (\x y. x)
to get the first element of the pair. Of course you can lift that if you want:
extract_fst ≡ \pair. pair (\x y. x)
but at a lower level, the pair is still accepting its handler as an argument,
rather than the handler taking the pair as an argument. (The handler gets *the
pair's elements*, not the pair itself, as arguments.)
> *Terminology*: we'll try to use names of the form `get_foo` for handlers, and
names of the form `extract_foo` for lifted versions of them, that accept the
lists (or whatever data structure we're working with) as arguments. But we may
sometimes forget.
The v2 implementation of lists followed a similar strategy:
v2list (\h t. do_something_with_h_and_t) result_if_empty
If the `v2list` here is not empty, then this will reduce to the result of
supplying the list's head and tail to the handler `(\h t.
do_something_with_h_and_t)`.
Now, what we've been imagining ourselves doing with the search through the v3
list is something like this:
larger_computation (search_through_the_list_for_3) other_arguments
That is, the result of our search is supplied as an argument (perhaps together
with other arguments) to the "larger computation". Without knowing the
evaluation order/reduction strategy, we can't say whether the search is
evaluated before or after it's substituted into the larger computation. But
semantically, the search is the argument and the larger computation is the
function to which it's supplied.
What if, instead, we did the same kind of thing we did with pairs and v2
lists? That is, what if we made the larger computation a "handler" that we
passed as an argument to the search?
the_search (\search_result. larger_computation search_result other_arguments)
What's the advantage of that, you say. Other than to show off how cleverly
you can lift.
Well, think about it. Think about the difficulty we were having aborting the
search. Does this switch-around offer us anything useful?
It could.
What if the way we implemented the search procedure looked something like this?
At a given stage in the search, we wouldn't just apply some function `f` to the
head at this stage and the result accumulated so far (from folding the same
function, and a base value, to the tail at this stage)...and then pass the result
of that application to the embedding, more leftward computation.
We'd *instead* give `f` a "handler" that expects the result of the current
stage *as an argument*, and then evaluates to what you'd get by passing that
result leftwards up the list, as before.
Why would we do that, you say? Just more flamboyant lifting?
Well, no, there's a real point here. If we give the function a "handler" that
encodes the normal continuation of the fold leftwards through the list, we can
also give it other "handlers" too. For example, we can also give it the underlined handler:
the_search (\search_result. larger_computation search_result other_arguments)
------------------------------------------------------------------
This "handler" encodes the search's having finished, and delivering a final
answer to whatever else you wanted your program to do with the result of the
search. If you like, at any stage in the search you might just give an argument
to *this* handler, instead of giving an argument to the handler that continues
the list traversal leftwards. Semantically, this would amount to *aborting* the
list traversal! (As we've said before, whether the rest of the list traversal
really gets evaluated will depend on what evaluation order is in place. But
semantically we'll have avoided it. Our larger computation won't depend on the
rest of the list traversal having been computed.)
Do you have the basic idea? Think about how you'd implement it. A good
understanding of the v2 lists will give you a helpful model.
In broad outline, a single stage of the search would look like before, except
now `f` would receive two extra, "handler" arguments. We'll reserve the name `f` for the original fold function, and use `f2` for the function that accepts two additional handler arguments. To get the general idea, you can regard these as interchangeable. If the extra precision might help, then you can pay attention to when we're talking about the handler-taking `f2` or the original `f`. You'll only be *supplying* the `f2` function; the idea will be that the behavior of the original `f` will be implicitly encoded in `f2`'s behavior.
f2 3
`f2`'s job would be to check whether `3` matches the element we're searching for
(here also `3`), and if it does, just evaluate to the result of passing `true` to
the abort handler. If it doesn't, then evaluate to the result of passing
`false` to the continue-leftwards handler.
In this case, `f2` wouldn't need to consult the result of folding `f` and `z`
over `[2; 1]`, since if we had found the element `3` in more rightward
positions of the list, we'd have called the abort handler and this application
of `f2` to `3` etc would never be needed. However, in other applications the
result of folding `f` and `z` over the more rightward parts of the list would
be needed. Consider if you were trying to multiply all the elements of the
list, and were going to abort (with the result `0`) if you came across any
element in the list that was zero. If you didn't abort, you'd need to know what
the more rightward elements of the list multiplied to, because that would
affect the answer you passed along to the continue-leftwards handler.
A **version 5** list encodes the kind of fold operation we're envisaging here,
in the same way that v3 (and [v4](/advanced_lambda/#index1h1)) lists encoded
the simpler fold operation. Roughly, the list `[5;4;3;2;1]` would look like
this:
\f2 z continue_leftwards_handler abort_handler.
(\result_of_folding_over_4321. f2 5 result_of_folding_over_4321 continue_leftwards_handler abort_handler)
abort_handler
; or, expanding the fold over [4;3;2;1]:
\f2 z continue_leftwards_handler abort_handler.
(\continue_leftwards_handler abort_handler.
(\result_of_folding_over_321. f2 4 result_of_folding_over_321 continue_leftwards_handler abort_handler)
abort_handler
)
(\result_of_folding_over_4321. f2 5 result_of_folding_over_4321 continue_leftwards_handler abort_handler)
abort_handler
; and so on
Remarks: the `larger_computation` handler should be supplied as both the
`continue_leftwards_handler` and the `abort_handler` for the leftmost
application, where the head `5` is supplied to `f2`; because the result of this
application should be passed to the larger computation, whether it's a "fall
off the left end of the list" result or it's a "I'm finished, possibly early"
result. The `larger_computation` handler also then gets passed to the next
rightmost stage, where the head `4` is supplied to `f2`, as the `abort_handler` to
use if that stage decides it has an early answer.
Finally, notice that we're not supplying the application of `f2` to `4` etc as an argument to the application of `f2` to `5` etc---at least, not directly. Instead, we pass
(\result_of_folding_over_4321. f2 5 result_of_folding_over_4321 )
*to* the application of `f2` to `4` as its "continue" handler. The application of `f2`
to `4` can decide whether this handler, or the other, "abort" handler, should be
given an argument and constitute its result.
I'll say once again: we're using temporally-loaded vocabulary throughout this,
but really all we're in a position to mean by that are claims about the result
of the complex expression semantically depending only on this, not on that. A
demon evaluator who custom-picked the evaluation order to make things maximally
bad for you could ensure that all the semantically unnecessary computations got
evaluated anyway. We don't yet know any way to prevent that. Later, we'll see
ways to *guarantee* one evaluation order rather than another. Of
course, in any real computing environment you'll know in advance that you're
dealing with a fixed evaluation order and you'll be able to program efficiently
around that.
In detail, then, here's what our v5 lists will look like:
let empty = \f2 z continue_handler abort_handler. continue_handler z in
let make_list = \h t. \f2 z continue_handler abort_handler.
t f2 z (\sofar. f2 h sofar continue_handler abort_handler) abort_handler in
let isempty = \lst larger_computation. lst
; here's our f2
(\hd sofar continue_handler abort_handler. abort_handler false)
; here's our z
true
; here's the continue_handler for the leftmost application of f2
larger_computation
; here's the abort_handler
larger_computation in
let extract_head = \lst larger_computation. lst
; here's our f2
(\hd sofar continue_handler abort_handler. continue_handler hd)
; here's our z
junk
; here's the continue_handler for the leftmost application of f2
larger_computation
; here's the abort_handler
larger_computation in
let extract_tail = ; left as exercise
These functions are used like this:
let my_list = make_list a (make_list b (make_list c empty) in
extract_head my_list larger_computation
If you just want to see `my_list`'s head, the use `I` as the
`larger_computation`.
What we've done here does take some work to follow. But it should be within
your reach. And once you have followed it, you'll be well on your way to
appreciating the full terrible power of continuations.
Of course, like everything elegant and exciting in this seminar, [Oleg
discusses it in much more
detail](http://okmij.org/ftp/Streams.html#enumerator-stream).
> *Comments*:
> 1. The technique deployed here, and in the v2 lists, and in our
> implementations of pairs and booleans, is known as
> **continuation-passing style** programming.
> 2. We're still building the list as a right fold, so in a sense the
> application of `f2` to the leftmost element `5` is "outermost". However,
> this "outermost" application is getting lifted, and passed as a *handler*
> to the next right application. Which is in turn getting lifted, and
> passed to its next right application, and so on. So if you
> trace the evaluation of the `extract_head` function to the list `[5;4;3;2;1]`,
> you'll see `1` gets passed as a "this is the head sofar" answer to its
> `continue_handler`; then that answer is discarded and `2` is
> passed as a "this is the head sofar" answer to *its* `continue_handler`,
> and so on. All those steps have to be evaluated to finally get the result
> that `5` is the outer/leftmost head of the list. That's not an efficient way
> to get the leftmost head.
>
> We could improve this by building lists as **left folds**. What's that?
>
> Well, the right fold of `f` over a list `[a;b;c;d;e]`, using starting value z, is:
>
> f a (f b (f c (f d (f e z))))
>
> The left fold on the other hand starts combining `z` with elements from the left. `f z a` is then combined with `b`, and so on:
>
> f (f (f (f (f z a) b) c) d) e
>
> or, if we preferred the arguments to each `f` flipped:
>
> f e (f d (f c (f b (f a z))))
>
> Recall we implemented v3 lists as their own right-fold functions. We could
> instead implement lists as their own left-fold functions. To do that with our
> v5 lists, we'd replace above:
>
> let make_list = \h t. \f2 z continue_handler abort_handler.
> f2 h z (\z. t f2 z continue_handler abort_handler) abort_handler
>
> Having done that, now `extract_head` can return the leftmost head
> directly, using its `abort_handler`:
>
> let extract_head = \lst larger_computation. lst
> (\hd sofar continue_handler abort_handler. abort_handler hd)
> junk
> larger_computation
> larger_computation
>
> 3. To extract tails efficiently, too, it'd be nice to fuse the apparatus
> developed in these v5 lists with the ideas from
> [v4](/advanced_lambda/#index1h1) lists. But that is left as an exercise.