post two images
[lambda.git] / topics / week12_abortable_traversals.mdwn
index 199c240..7cccacb 100644 (file)
@@ -1,10 +1,12 @@
-## Aborting a search through a list ##
-
 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 instead using our right-fold or left-fold implementation of lists, instead of resorting to `letrec` or a fixed point combinator? In those cases, 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. This will work fine, 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. When we find a result mid-way through the traversal, we want to be able to just return that result and have the traversal then be _finished_.
+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:
 
@@ -19,11 +21,21 @@ and we worked out that a left-fold implementation of the list `[10,20,30,40]` co
 
     \f z done_handler. f 10 z done_handler (\z. [20,30,40] f z done_handler)
 
-The only surprising bit here is that the `keep_going_handler` we supply the traversal function `f` with encodes how the traversal should continue, using the updated seed value `z` from this step, without actually computing the rest of the traversal. It's up to `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.
+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.)
 
-A question came up in the session of why we need the `done_handler` in these schemes. 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 in this case, this is correct. (In a few weeks when we look at delimited continuations in terms of `reset`/`shift`, you'll see that this is essentially how the `abort` operation gets implemented using `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.
+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.
 
-With this general scheme, here is the empty list:
+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
 
@@ -51,9 +63,10 @@ Here's how to get the last element of such a list:
 
 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.
 
-All of this 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.)
+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.
 
-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):
+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)
 
@@ -71,7 +84,7 @@ Now suppose we had just the implementation of the tail of the list, `[20,30,40]`
 
 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 the fold function we implement the list as a single handler. 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:
+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))
 
@@ -89,10 +102,18 @@ Spelling this out, here are the implementations of the functions we defined befo
 
 *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|week3_lists#v3-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](http://okmij.org/ftp/Streams.html#enumerator-stream).
+
+
 
----
 
 
+<!--
+
 We said that the sorted-list implementation of a set was more efficient than
 the unsorted-list implementation, because as you were searching through the
 list, you could come to a point where you knew the element wasn't going to be
@@ -391,11 +412,8 @@ 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.
 
-<!-- (Silly [cultural reference](http://www.newgrounds.com/portal/view/33440).) -->
+(Silly [cultural reference](http://www.newgrounds.com/portal/view/33440).)
 
-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*:
 
@@ -450,3 +468,4 @@ detail](http://okmij.org/ftp/Streams.html#enumerator-stream).
 >      developed in these v5 lists with the ideas from 
 >      [v4](/advanced_lambda/#index1h1) lists. But that is left as an exercise.
 
+-->