some rewrites, not finished
[lambda.git] / topics / week12_abortable_traversals.mdwn
index 6976a51..199c240 100644 (file)
@@ -1,4 +1,97 @@
-#Aborting a search through a list#
+## 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_.
+
+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)
+
+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.
+
+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.
+
+With this 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.
+
+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.)
+
+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 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:
+
+    \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?
+
+
+---
+
 
 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