X-Git-Url: http://lambda.jimpryor.net/git/gitweb.cgi?a=blobdiff_plain;ds=inline;f=week4.mdwn;h=150e88354cd7f8b56884ffe201e3afd1255693ba;hb=2c604a7f018e6e2af314f39a07dce0a15f1d9cb8;hp=8fe37ad2be182d92c8cc1298794ced2cc6a99b9d;hpb=c3998b2b5a85bd4b5a47c1b2518739fe9948807f;p=lambda.git diff --git a/week4.mdwn b/week4.mdwn index 8fe37ad2..150e8835 100644 --- a/week4.mdwn +++ b/week4.mdwn @@ -6,75 +6,82 @@ A: That's easy: let `T` be an arbitrary term in the lambda calculus. If `T` has a fixed point, then there exists some `X` such that `X <~~> TX` (that's what it means to *have* a fixed point). -
-let W = \x.T(xx) in -let X = WW in -X = WW = (\x.T(xx))W = T(WW) = TX -+
let L = \x. T (x x) in
+let X = L L in
+X ≡ L L ≡ (\x. T (x x)) L ~~> T (L L) ≡ T X
+
Please slow down and make sure that you understand what justified each
of the equalities in the last line.
-#Q: How do you know that for any term `T`, `YT` is a fixed point of `T`?#
+#Q: How do you know that for any term `T`, `Y T` is a fixed point of `T`?#
A: Note that in the proof given in the previous answer, we chose `T`
-and then set `X = WW = (\x.T(xx))(\x.T(xx))`. If we abstract over
-`T`, we get the Y combinator, `\T.(\x.T(xx))(\x.T(xx))`. No matter
-what argument `T` we feed Y, it returns some `X` that is a fixed point
+and then set X ≡ L L ≡ (\x. T (x x)) (\x. T (x x))
. If we abstract over
+`T`, we get the Y combinator, `\T. (\x. T (x x)) (\x. T (x x))`. No matter
+what argument `T` we feed `Y`, it returns some `X` that is a fixed point
of `T`, by the reasoning in the previous answer.
#Q: So if every term has a fixed point, even `Y` has fixed point.#
A: Right:
- let Y = \T.(\x.T(xx))(\x.T(xx)) in
- Y Y = \T.(\x.T(xx))(\x.T(xx)) Y
- = (\x.Y(xx))(\x.Y(xx))
- = Y((\x.Y(xx))(\x.Y(xx)))
- = Y(Y((\x.Y(xx))(\x.Y(xx))))
- = Y(Y(Y(...(Y(YY))...)))
+let Y = \T. (\x. T (x x)) (\x. T (x x)) in
+Y Y
+≡ \T. (\x. T (x x)) (\x. T (x x)) Y
+~~> (\x. Y (x x)) (\x. Y (x x))
+~~> Y ((\x. Y (x x)) (\x. Y (x x)))
+~~> Y (Y ((\x. Y (x x)) (\x. Y (x x))))
+~~> Y (Y (Y (...(Y (Y Y))...)))
+
+
#Q: Ouch! Stop hurting my brain.#
-A: Let's come at it from the direction of arithmetic. Recall that we
+A: Is that a question?
+
+Let's come at it from the direction of arithmetic. Recall that we
claimed that even `succ`---the function that added one to any
number---had a fixed point. How could there be an X such that X = X+1?
That would imply that
- X = succ X = succ (succ X) = succ (succ (succ (X))) = succ (... (succ X)...)
+ X <~~> succ X <~~> succ (succ X) <~~> succ (succ (succ X)) <~~> succ (... (succ X)...)
In other words, the fixed point of `succ` is a term that is its own
successor. Let's just check that `X = succ X`:
- let succ = \n s z. s (n s z) in
- let X = (\x.succ(xx))(\x.succ(xx)) in
- succ X
- = succ ((\x.succ(xx))(\x.succ(xx)))
- = succ (succ ((\x.succ(xx))(\x.succ(xx))))
- = succ (succ X)
+let succ = \n s z. s (n s z) in
+let X = (\x. succ (x x)) (\x. succ (x x)) in
+succ X
+≡ succ ( (\x. succ (x x)) (\x. succ (x x)) )
+~~> succ (succ ( (\x. succ (x x)) (\x. succ (x x)) ))
+≡ succ (succ X)
+
+
+You should see the close similarity with `Y Y` here.
-You should see the close similarity with YY here.
#Q. So `Y` applied to `succ` returns a number that is not finite!#
A. Yes! Let's see why it makes sense to think of `Y succ` as a Church
numeral:
- [same definitions]
- succ X
- = (\n s z. s (n s z)) X
- = \s z. s (X s z)
- = succ (\s z. s (X s z)) ; using fixed-point reasoning
- = \s z. s ([succ (\s z. s (X s z))] s z)
- = \s z. s ([\s z. s ([succ (\s z. s (X s z))] s z)] s z)
- = \s z. s (s (succ (\s z. s (X s z))))
+[same definitions]
+succ X
+≡ (\n s z. s (n s z)) X
+~~> \s z. s (X s z)
+<~~> succ (\s z. s (X s z)) ; using fixed-point reasoning
+≡ (\n s z. s (n s z)) (\s z. s (X s z))
+~~> \s z. s ((\s z. s (X s z)) s z)
+~~> \s z. s (s (X s z))
+
So `succ X` looks like a numeral: it takes two arguments, `s` and `z`,
and returns a sequence of nested applications of `s`...
You should be able to prove that `add 2 (Y succ) <~~> Y succ`,
-likewise for `mult`, `minus`, `pow`. What happens if we try `minus (Y
-succ)(Y succ)`? What would you expect infinity minus infinity to be?
+likewise for `mul`, `sub`, `pow`. What happens if we try `sub (Y
+succ) (Y succ)`? What would you expect infinity minus infinity to be?
(Hint: choose your evaluation strategy so that you add two `s`s to the
first number for every `s` that you add to the second number.)
@@ -84,13 +91,14 @@ represents arithmetic infinity.
It's important to bear in mind the simplest term in question is not
infinite:
- Y succ = (\f.(\x.f(xx))(\x.f(xx)))(\n s z. s (n s z))
+ Y succ = (\f. (\x. f (x x)) (\x. f (x x))) (\n s z. s (n s z))
The way that infinity enters into the picture is that this term has
no normal form: no matter how many times we perform beta reduction,
there will always be an opportunity for more beta reduction. (Lather,
rinse, repeat!)
+
#Q. That reminds me, what about [[evaluation order]]?#
A. For a recursive function that has a well-behaved base case, such as
@@ -100,63 +108,63 @@ which we have to make a choice about which beta reduction to perform
next: one choice leads to a normal form, the other choice leads to
endless reduction:
- let prefac = \f n. isZero n 1 (mult n (f (pred n))) in
- let fac = Y prefac in
- fac 2
- = [(\f.(\x.f(xx))(\x.f(xx))) prefac] 2
- = [(\x.prefac(xx))(\x.prefac(xx))] 2
- = [prefac((\x.prefac(xx))(\x.prefac(xx)))] 2
- = [prefac(prefac((\x.prefac(xx))(\x.prefac(xx))))] 2
- = [(\f n. isZero n 1 (mult n (f (pred n))))
- (prefac((\x.prefac(xx))(\x.prefac(xx))))] 2
- = [\n. isZero n 1 (mult n ([prefac((\x.prefac(xx))(\x.prefac(xx)))] (pred n)))] 2
- = isZero 2 1 (mult 2 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] (pred 2)))
- = mult 2 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] 1)
- ...
- = mult 2 (mult 1 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] 0))
- = mult 2 (mult 1 (isZero 0 1 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] (pred 0))))
- = mult 2 (mult 1 1)
- = mult 2 1
- = 2
+let prefact = \f n. iszero n 1 (mul n (f (pred n))) in
+let fact = Y prefact in
+fact 2
+≡ [(\f. (\x. f (x x)) (\x. f (x x))) prefact] 2
+~~> [(\x. prefact (x x)) (\x. prefact (x x))] 2
+~~> [prefact ((\x. prefact (x x)) (\x. prefact (x x)))] 2
+~~> [prefact (prefact ((\x. prefact (x x)) (\x. prefact (x x))))] 2
+≡ [ (\f n. iszero n 1 (mul n (f (pred n)))) (prefact ((\x. prefact (x x)) (\x. prefact (x x))))] 2
+~~> [\n. iszero n 1 (mul n ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] (pred n)))] 2
+~~> iszero 2 1 (mul 2 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] (pred 2)))
+~~> mul 2 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] 1)
+...
+~~> mul 2 (mul 1 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] 0))
+≡ mul 2 (mul 1 (iszero 0 1 (mul 1 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] (pred 0)))))
+~~> mul 2 (mul 1 1)
+~~> mul 2 1
+~~> 2
+
The crucial step is the third from the last. We have our choice of
-either evaluating the test `isZero 0 1 ...`, which evaluates to `1`,
+either evaluating the test `iszero 0 1 ...`, which evaluates to `1`,
no matter what the ... contains;
-or we can evaluate the `Y` pump, `(\x.prefac(xx))(\x.prefac(xx))`, to
-produce another copy of `prefac`. If we postpone evaluting the
-`isZero` test, we'll pump out copy after copy of `prefac`, and never
+or we can evaluate the `Y` pump, `(\x. prefact (x x)) (\x. prefact (x x))`, to
+produce another copy of `prefact`. If we postpone evaluting the
+`iszero` test, we'll pump out copy after copy of `prefact`, and never
realize that we've bottomed out in the recursion. But if we adopt a
leftmost/call-by-name/normal-order evaluation strategy, we'll always
-start with the `isZero` predicate, and only produce a fresh copy of
-`prefac` if we are forced to.
+start with the `iszero` predicate, and only produce a fresh copy of
+`prefact` if we are forced to.
+
#Q. You claimed that the Ackerman function couldn't be implemented using our primitive recursion techniques (such as the techniques that allow us to define addition and multiplication). But you haven't shown that it is possible to define the Ackerman function using full recursion.#
+
A. OK:
--A(m,n) = - | when m == 0 -> n + 1 - | else when n == 0 -> A(m-1,1) - | else -> A(m-1, A(m,n-1)) - -let A = Y (\A m n. isZero m (succ n) (isZero n (A (pred m) 1) (A (pred m) (A m (pred n))))) in -- -For instance, - - A 1 2 - = A 0 (A 1 1) - = A 0 (A 0 (A 1 0)) - = A 0 (A 0 (A 0 1)) - = A 0 (A 0 2) - = A 0 3 - = 4 - -A 1 x is to A 0 x as addition is to the successor function; -A 2 x is to A 1 x as multiplication is to addition; -A 3 x is to A 2 x as exponentiation is to multiplication--- -so A 4 x is to A 3 x as hyper-exponentiation is to exponentiation... + A(m,n) = + | when m == 0 -> n + 1 + | else when n == 0 -> A(m-1,1) + | else -> A(m-1, A(m,n-1)) + + let A = Y (\A m n. iszero m (succ n) (iszero n (A (pred m) 1) (A (pred m) (A m (pred n))))) + +So for instance: + + A 1 2 + ~~> A 0 (A 1 1) + ~~> A 0 (A 0 (A 1 0)) + ~~> A 0 (A 0 (A 0 1)) + ~~> A 0 (A 0 2) + ~~> A 0 3 + ~~> 4 + +`A 1 x` is to `A 0 x` as addition is to the successor function; +`A 2 x` is to `A 1 x` as multiplication is to addition; +`A 3 x` is to `A 2 x` as exponentiation is to multiplication--- +so `A 4 x` is to `A 3 x` as hyper-exponentiation is to exponentiation... #Q. What other questions should I be asking?# @@ -173,129 +181,427 @@ so A 4 x is to A 3 x as hyper-exponentiation is to exponentiation... Is leastness important? -##The simply-typed lambda calculus## - -The uptyped lambda calculus is pure computation. It is much more -common, however, for practical programming languages to be typed. -Likewise, systems used to investigate philosophical or linguistic -issues are almost always typed. Types will help us reason about our -computations. They will also facilitate a connection between logic -and computation. - -Soon we will consider polymorphic type systems. First, however, we -will consider the simply-typed lambda calculus. There's good news and -bad news: the good news is that the simply-type lambda calculus is -strongly normalizing: every term has a normal form. We shall see that -self-application is outlawed, so Ω can't even be written, let -alone undergo reduction. The bad news is that fixed-point combinators -are also forbidden, so recursion is neither simple nor direct. - -#Types# - -We will have at least one ground type, `o`. From a linguistic point -of view, think of the ground types as the bar-level 0 categories, that -is, the lexical types, such as Noun, Verb, Preposition (glossing over -the internal complexity of those categories in modern theories). - -In addition, there will be a recursively-defined class of complex -types `T`, the smallest set such that - -* ground types, including `o`, are in `T` - -* for any types σ and τ in `T`, the type σ --> - τ is in `T`. - -For instance, here are some types in `T`: - o - o --> o - o --> o --> o - (o --> o) --> o - (o --> o) --> o --> o +#Sets# + +You're now already in a position to implement sets: that is, collections with +no intrinsic order where elements can occur at most once. Like lists, we'll +understand the basic set structures to be *type-homogenous*. So you might have +a set of integers, or you might have a set of pairs of integers, but you +wouldn't have a set that mixed both types of elements. Something *like* the +last option is also achievable, but it's more difficult, and we won't pursue it +now. In fact, we won't talk about sets of pairs, either. We'll just talk about +sets of integers. The same techniques we discuss here could also be applied to +sets of pairs of integers, or sets of triples of booleans, or sets of pairs +whose first elements are booleans, and whose second elements are triples of +integers. And so on. + +(You're also now in a position to implement *multi*sets: that is, collections +with no intrinsic order where elements can occur multiple times: the multiset +{a,a} is distinct from the multiset {a}. But we'll leave these as an exercise.) + +The easiest way to implement sets of integers would just be to use lists. When +you "add" a member to a set, you'd get back a list that was either identical to +the original list, if the added member already was present in it, or consisted +of a new list with the added member prepended to the old list. That is: + + let empty_set = empty in + ; see the library for definitions of any and eq + let make_set = \new_member old_set. any (eq new_member) old_set + ; if any element in old_set was eq new_member + old_set + ; else + make_list new_member old_set + +Think about how you'd implement operations like `set_union`, +`set_intersection`, and `set_difference` with this implementation of sets. + +The implementation just described works, and it's the simplest to code. +However, it's pretty inefficient. If you had a 100-member set, and you wanted +to create a set which had all those 100-members and some possibly new element +`e`, you might need to check all 100 members to see if they're equal to `e` +before concluding they're not, and returning the new list. And comparing for +numeric equality is a moderately expensive operation, in the first place. + +(You might say, well, what's the harm in just prepending `e` to the list even +if it already occurs later in the list. The answer is, if you don't keep track +of things like this, it will likely mess up your implementations of +`set_difference` and so on. You'll have to do the book-keeping for duplicates +at some point in your code. It goes much more smoothly if you plan this from +the very beginning.) + +How might we make the implementation more efficient? Well, the *semantics* of +sets says that they have no intrinsic order. That means, there's no difference +between the set {a,b} and the set {b,a}; whereas there is a difference between +the *list* `[a;b]` and the list `[b;a]`. But this semantic point can be respected +even if we *implement* sets with something ordered, like list---as we're +already doing. And we might *exploit* the intrinsic order of lists to make our +implementation of sets more efficient. + +What we could do is arrange it so that a list that implements a set always +keeps in elements in some specified order. To do this, there'd have *to be* +some way to order its elements. Since we're talking now about sets of numbers, +that's easy. (If we were talking about sets of pairs of numbers, we'd use +"lexicographic" ordering, where `(a,b) < (c,d)` iff `a < c or (a == c and b < +d)`.) + +So, if we were searching the list that implements some set to see if the number +`5` belonged to it, once we get to elements in the list that are larger than `5`, +we can stop. If we haven't found `5` already, we know it's not in the rest of the +list either. + +*Comment*: This is an improvement, but it's still a "linear" search through the list. +There are even more efficient methods, which employ "binary" searching. They'd +represent the set in such a way that you could quickly determine whether some +element fell in one half, call it the left half, of the structure that +implements the set, if it belonged to the set at all. Or that it fell in the +right half, it it belonged to the set at all. And then the same sort of +determination could be made for whichever half you were directed to. And then +for whichever quarter you were directed to next. And so on. Until you either +found the element or exhausted the structure and could then conclude that the +element in question was not part of the set. These sorts of structures are done +using [binary trees](/implementing_trees). + + +#Aborting a search through a list# + +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 +found. So you wouldn't have to continue the search. + +If your implementation of lists was, say v1 lists plus the Y-combinator, then +this is exactly right. When you get to a point where you know the answer, you +can just deliver that answer, and not branch into any further recursion. If +you've got the right evaluation strategy in place, everything will work out +fine. + +-- +An advantage of the v3 lists and v3 (aka "Church") numerals is that they +have a recursive capacity built into their skeleton. So for many natural +operations on them, you won't need to use a fixed point combinator. Why is +that an advantage? Well, if you use a fixed point combinator, then the terms +you get +won't be strongly normalizing: whether their reduction stops at a normal form +will depend on what evaluation order you use. Our online [[lambda evaluator]] +uses normal-order reduction, so it finds a normal form if there's one to be +had. But if you want to build lambda terms in, say, Scheme, and you wanted to +roll your own recursion as we've been doing, rather than relying on Scheme's +native `let rec` or `define`, then you can't use the fixed-point combinators +`Y` or
Θ
. Expressions using them will have non-terminating
+reductions, with Scheme's eager/call-by-value strategy. There are other
+fixed-point combinators you can use with Scheme (in the [week 3 notes](/week3/#index7h2) they
+were Y′
and Θ′
. But even with
+them, evaluation order still matters: for some (admittedly unusual)
+evaluation strategies, expressions using them will also be non-terminating.
+
+The fixed-point combinators may be the conceptual stars. They are cool and
+mathematically elegant. But for efficiency and implementation elegance, it's
+best to know how to do as much as you can without them. (Also, that knowledge
+could carry over to settings where the fixed point combinators are in
+principle unavailable.)
+
+This is why the v3 lists and numbers are so lovely..
+
+--
+
+But what if you're using v3 lists? What options would you have then for
+aborting a search?
+
+Well, suppose we're searching through the list `[5;4;3;2;1]` to see if it
+contains the number `3`. The expression which represents this search would have
+something like the following form:
+
+ ..................&Lamda;_T
,
-which is the smallest set such that
+ 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.)
-* each type `t` has an infinite set of distinct variables, {x^t}_1,
- {x^t}_2, {x^t}_3, ...
+> *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.
-* If a term `M` has type σ --> τ, and a term `N` has type
- σ, then the application `(M N)` has type τ.
+The v2 implementation of lists followed a similar strategy:
-* If a variable `a` has type σ, and term `M` has type τ,
- then the abstract `λ a M` has type `σ --> τ`.
+ v2list (\h t. do_something_with_h_and_t) result_if_empty
-The definitions of types and of typed terms should be highly familiar
-to semanticists, except that instead of writing `σ --> τ`,
-linguists (following Montague, who followed Church) write `<σ,
-τ>`. We will use the arrow notation, since it is more iconic.
+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)`.
-Some examples (assume that `x` has type `o`):
+Now, what we've been imagining ourselves doing with the search through the v3
+list is something like this:
- x o
- \x.x o --> o
- ((\x.x) x) o
-Excercise: write down terms that have the following types:
+ larger_computation (search_through_the_list_for_3) other_arguments
- o --> o --> o
- (o --> o) --> o --> o
- (o --> o --> o) --> o
+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.
-#Associativity of types versus terms#
+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?
-As we have seen many times, in the lambda calculus, function
-application is left associative, so that `f x y z == (((f x) y) z)`.
-Types, *THEREFORE*, are right associative: if `f`, `x`, `y`, and `z`
-have types `a`, `b`, `c`, and `d`, respectively, then `f` has type `a
---> b --> c --> d == (a --> (b --> (c --> d)))`.
+ the_search (\search_result. larger_computation search_result other_arguments)
-It is a serious faux pas to associate to the left for types, on a par
-with using your salad fork to stir your tea.
+What's the advantage of that, you say. Other than to show off how cleverly
+you can lift.
-#The simply-typed lambda calculus is strongly normalizing#
+Well, think about it. Think about the difficulty we were having aborting the
+search. Does this switch-around offer us anything useful?
-If `M` is a term with type τ in `Λ_T`, then `M` has a
-normal form. The proof is not particularly complex, but we will not
-present it here; see Berendregt or Hankin.
+It could.
-Since Ω does not have a normal form, it follows that Ω
-cannot have a type in `Λ_T`. We can easily see why:
+What if the way we implemented the search procedure looked something like this?
- Ω = (\x.xx)(\x.xx)
+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.
-Assume Ω has type τ, and `\x.xx` has type σ. Then
-because `\x.xx` takes an argument of type σ and returns
-something of type τ, `\x.xx` must also have type `σ -->
-τ`. By repeating this reasoning, `\x.xx` must also have type
-`(σ --> τ) --> τ`; and so on. Since variables have
-finite types, there is no way to choose a type for the variable `x`
-that can satisfy all of the requirements imposed on it.
+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.
-In general, there is no way for a function to have a type that can
-take itself for an argument. It follows that there is no way to
-define the identity function in such a way that it can take itself as
-an argument. Instead, there must be many different identity
-functions, one for each type.
+Why would we do that, you say? Just more flamboyant lifting?
-#Typing numerals#
+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:
-Version 1 type numerals are not a good choice for the simply-typed
-lambda calculus. The reason is that each different numberal has a
-different type! For instance, if zero has type σ, and `false`
-has type `τ --> τ --> τ` for some τ, and one is
-represented by the function `\x.x false 0`, then one must have type
-`(τ --> τ --> &tau) --> &sigma --> σ`. But this is a
-different type than zero! Because numbers have different types, it
-becomes impossible to write arithmetic operations that can combine
-zero with one. We would need as many different addition operations as
-we had pairs of numbers that we wanted to add.
-Fortunately, the Church numberals are well behaved with respect to
-types. They can all be given the type `(σ --> σ) -->
-σ --> σ`.
+ 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.
+
+ f 3