-1. How would you define an operation to reverse a list? (Don't peek at the
+[[!toc]]
+
+#Reversing a list#
+
+How would you define an operation to reverse a list? (Don't peek at the
[[lambda_library]]! Try to figure it out on your own.) Choose whichever
implementation of list you like. Even then, there are various strategies you
can use.
-2. 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 <code>Θ</code>. 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 <code>Y′</code> and <code>Θ′</code>. But even with
- them, evaluation order still matters: for some (admittedly unusual)
- evaluation strategies, expressions using them will also be non-terminating.
+#Efficiently extracting tails and predecessors#
- 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.)
+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 <code>Θ</code>. 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 <code>Y′</code> and <code>Θ′</code>. But even with
+them, evaluation order still matters: for some (admittedly unusual)
+evaluation strategies, expressions using them will also be non-terminating.
- This is why the v3 lists and numbers are so lovely. However, one disadvantage
- to them is that it's relatively inefficient to extract a list's tail, or get a
- number's predecessor. To get the tail of the list `[a;b;c;d;e]`, one will
- basically be performing some operation that builds up the tail afresh: at
- different stages, one will have built up `[e]`, then `[d;e]`, then `[c;d;e]`, and
- finally `[b;c;d;e]`. With short lists, this is no problem, but with longer lists
- it takes longer and longer. And it may waste more of your computer's memory
- than you'd like. Similarly for obtaining a number's predecessor.
+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.)
- The v1 lists and numbers on the other hand, had the tail and the predecessor
- right there as an element, easy for the taking. The problem was just that the
- v1 lists and numbers didn't have recursive capacity built into them, in the
- way the v3 implementations do.
+This is why the v3 lists and numbers are so lovely. However, one disadvantage
+to them is that it's relatively inefficient to extract a list's tail, or get a
+number's predecessor. To get the tail of the list `[a;b;c;d;e]`, one will
+basically be performing some operation that builds up the tail afresh: at
+different stages, one will have built up `[e]`, then `[d;e]`, then `[c;d;e]`, and
+finally `[b;c;d;e]`. With short lists, this is no problem, but with longer lists
+it takes longer and longer. And it may waste more of your computer's memory
+than you'd like. Similarly for obtaining a number's predecessor.
- A clever approach would marry these two strategies.
+The v1 lists and numbers on the other hand, had the tail and the predecessor
+right there as an element, easy for the taking. The problem was just that the
+v1 lists and numbers didn't have recursive capacity built into them, in the
+way the v3 implementations do.
- Version 3 makes the list `[a;b;c;d;e]` look like this:
+A clever approach would marry these two strategies.
- \f z. f a (f b (f c (f d (f e z))))
+Version 3 makes the list `[a;b;c;d;e]` look like this:
- or in other words:
+ \f z. f a (f b (f c (f d (f e z))))
- \f z. f a <the result of folding f and z over the tail>
+or in other words:
- Instead we could make it look like this:
+ \f z. f a <the result of folding f and z over the tail>
- \f z. f a <the tail itself> <the result of folding f and z over the tail>
+Instead we could make it look like this:
- That is, now `f` is a function expecting *three* arguments: the head of the
- current list, the tail of the current list, and the result of continuing to
- fold `f` over the tail, with a given base value `z`.
+ \f z. f a <the tail itself> <the result of folding f and z over the tail>
- Call this a **version 4** list. The empty list could be the same:
+That is, now `f` is a function expecting *three* arguments: the head of the
+current list, the tail of the current list, and the result of continuing to
+fold `f` over the tail, with a given base value `z`.
- empty === \f z. z
+Call this a **version 4** list. The empty list can be the same as in v3:
- The list constructor would be:
-
- make_list === \h t. \f z. f h t (t f z)
-
- It differs from the version 3 `make_list` only in adding the extra argument
- `t` to the new, outer application of `f`.
-
- Similarly, 5 as a v3 or Church numeral looks like this:
-
- \s z. s (s (s (s (s z))))
-
- or in other words:
+<pre><code>empty ≡ \f z. z</code></pre>
- \s z. s <the result of applying s to z (pred 5)-many times>
-
- Instead we could make it look like this:
+The list constructor would be:
- \s z. s <pred 5> <the result of applying s to z (pred 5)-many times>
-
- That is, now `s` is a function expecting *two* arguments: the predecessor of the
- current number, and the result of continuing to apply `s` to the base value `z`
- predecessor-many times.
-
- Jim had the pleasure of "inventing" these implementations himself. However,
- unsurprisingly, he wasn't the first to do so. See for example [Oleg's report
- on P-numerals](http://okmij.org/ftp/Computation/lambda-calc.html#p-numerals).
-
-
-
-3. **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 definition of any
- 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.
+<pre><code>make_list ≡ \h t. \f z. f h t (t f z)</code></pre>
+
+It differs from the version 3 `make_list` only in adding the extra argument
+`t` to the new, outer application of `f`.
+
+Similarly, `five` as a v3 or Church numeral looks like this:
+
+ \s z. s (s (s (s (s z))))
- (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.)
+or in other words:
- 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)`.)
+ \s z. s <the result of applying s to z (pred 5)-many times>
- 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.
-
- 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** (see below).
-
-
-4. **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.
-
- 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:
-
- ..................<eq? 1 3> ~~>
- .................. false ~~>
- .............<eq? 2 3> ~~>
- ............. false ~~>
- .........<eq? 3 3> ~~>
- ......... 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.
-
- Well, 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.
+Instead we could make it look like this:
- 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.
+ \s z. s <pred 5> <the result of applying s to z (pred 5)-many times>
- 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.
+That is, now `s` is a function expecting *two* arguments: the predecessor of the
+current number, and the result of continuing to apply `s` to the base value `z`
+predecessor-many times.
- 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:
+Jim had the pleasure of "inventing" these implementations himself. However,
+unsurprisingly, he wasn't the first to do so. See for example [Oleg's report
+on P-numerals](http://okmij.org/ftp/Computation/lambda-calc.html#p-numerals).
- pair (\x y. add x y)
- or:
- pair (\x y. x)
+#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.)
- to get the first element of the pair. Of course you can lift that if you want:
+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)`.)
- extract_1st === \pair. pair (\x y. x)
+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.
+
+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** (see below).
+
+
+#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.
+
+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:
+
+ ..................<eq? 1 3> ~~>
+ .................. false ~~>
+ .............<eq? 2 3> ~~>
+ ............. false ~~>
+ .........<eq? 3 3> ~~>
+ ......... 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.
+
+Well, 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.
- 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.)
+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.
- The v2 implementation of lists followed a similar strategy:
+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.
- v2list (\h t. do_something_with_h_and_t) result_if_empty
+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:
- 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)`.
+ pair (\x y. add x y)
- Now, what we've been imagining ourselves doing with the search through the v3
- list is something like this:
+or:
+ pair (\x y. x)
- larger_computation (search_through_the_list_for_3) other_arguments
+to get the first element of the pair. Of course you can lift that if you want:
- 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.
+<pre><code>extract_fst ≡ \pair. pair (\x y. x)</code></pre>
- 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?
+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.)
- the_search (\search_result. larger_computation search_result other_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.
- What's the advantage of that, you say. Other than to show off how cleverly
- you can lift.
+The v2 implementation of lists followed a similar strategy:
- Well, think about it. Think about the difficulty we were having aborting the
- search. Does this switch-around offer us anything useful?
+ v2list (\h t. do_something_with_h_and_t) result_if_empty
- It could.
+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)`.
- What if the way we implemented the search procedure looked something like this?
+Now, what we've been imagining ourselves doing with the search through the v3
+list is 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 doing so leftward along the rest of the list.
- We'd also give that function a "handler" that expected the result of the
- current stage as an argument, and evaluated to passing that result leftwards
- along the rest of the list.
+ larger_computation (search_through_the_list_for_3) other_arguments
- Why would we do that, you say? Just more flamboyant lifting?
+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.
- 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
- give it another "handler" as well. We can also give it the underlined handler:
+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)
- 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.
- 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.)
+Well, think about it. Think about the difficulty we were having aborting the
+search. Does this switch-around offer us anything useful?
- 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.
+It could.
- In broad outline, a single stage of the search would look like before, except
- now f would receive two extra, "handler" arguments.
+What if the way we implemented the search procedure looked something like this?
- f 3 <result of folding f and z over [2; 1]> <handler to continue folding leftwards> <handler to abort the traversal>
+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.
- `f`'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.
+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 this case, `f` 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 `f` 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.
+Why would we do that, you say? Just more flamboyant lifting?
- A **version 5** list would encode this kind of fold operation over the list, in
- the same way that v3 (and v4) lists encoded the simpler fold operation.
- Roughly, the list `[5;4;3;2;1]` would look like this:
+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:
- \f z continue_leftwards_handler abort_handler.
- <fold f and z over [4; 3; 2; 1]>
- (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 continue_leftwards_handler abort_handler)
- abort_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.
+
+ f 3 <result of folding f and z over [2; 1]> <handler to continue folding leftwards> <handler to abort the traversal>
+
+`f`'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, `f` 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 `f` 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) lists encoded the simpler fold operation.
+Roughly, the list `[5;4;3;2;1]` would look like this:
- \f z continue_leftwards_handler abort_handler.
- (\continue_leftwards_handler abort_handler.
- <fold f and z over [3; 2; 1]>
- (\result_of_fold_over_321. f 4 result_of_fold_over_321 continue_leftwards_handler abort_handler)
- abort_handler
- )
- (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 continue_leftwards_handler abort_handler)
+ \f z continue_leftwards_handler abort_handler.
+ <fold f and z over [4;3;2;1]>
+ (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 continue_leftwards_handler abort_handler)
+ abort_handler
+
+ ; or, expanding the fold over [4;3;2;1]:
+
+ \f z continue_leftwards_handler abort_handler.
+ (\continue_leftwards_handler abort_handler.
+ <fold f and z over [3;2;1]>
+ (\result_of_fold_over_321. f 4 result_of_fold_over_321 continue_leftwards_handler abort_handler)
abort_handler
+ )
+ (\result_of_fold_over_4321. f 5 result_of_fold_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 `f`; 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 `f`, as the `abort_handler` to
+use if that stage decides it has an early answer.
+
+Finally, notice that we don't have the result of applying `f` to `4` etc given as
+an argument to the application of `f` to `5` etc. Instead, we pass
+
+ (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 <one_handler> <another_handler>)
+
+*to* the application of `f` to `4` as its "continue" handler. The application of `f`
+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 have any way to prevent that. Later,
+we'll see ways to *semantically guarantee* one evaluation order rather than
+another. Though even then the demonic evaluation-order-chooser could make it
+take unnecessarily long to compute the semantically guaranteed result. Of
+course, in any real computing environment you'll know 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 = \f z continue_handler abort_handler. continue_handler z in
+ let make_list = \h t. \f z continue_handler abort_handler.
+ t f z (\sofar. f h sofar continue_handler abort_handler) abort_handler in
+ let isempty = \lst larger_computation. lst
+ ; here's our f
+ (\hd sofar continue_handler abort_handler. abort_handler false)
+ ; here's our z
+ true
+ ; here's the continue_handler for the leftmost application of f
+ larger_computation
+ ; here's the abort_handler
+ larger_computation in
+ let extract_head = \lst larger_computation. lst
+ ; here's our f
+ (\hd sofar continue_handler abort_handler. continue_handler hd)
+ ; here's our z
+ junk
+ ; here's the continue_handler for the leftmost application of f
+ 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.
+
+<!-- (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*:
+
+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 `f` 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 when implementing them
+ as continuation-passing style folds. We'd just replace above:
- 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 `f`. 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 `f`, as the `abort_handler` to
- use if that stage decides it has an early answer.
-
- Finally, notice that we don't have the result of applying `f` to `4` etc given as
- an argument to the application of `f` to `5` etc. Instead, we pass
-
- (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 one_handler another_handler)
-
- *to* the application of `f` to `4` as its "continue" handler. The application of `f`
- 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. At this stage, we don't have any way to prevent that. Later,
- we'll see ways to semantically guarantee one evaluation order rather than
- another. Though even then the demonic evaluation-order-chooser could make it
- take unnecessarily long to compute the semantically guaranteed result. Of
- course, in any real computing environment you'll know 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 = \f z continue_handler abort_handler. continue_handler z in
let make_list = \h t. \f z continue_handler abort_handler.
- t f z (\sofar. f h sofar continue_handler abort_handler) abort_handler in
- let isempty = \lst larger_computation. lst
- ; here's our f
- (\hd sofar continue_handler abort_handler. abort_handler false)
- ; here's our z
- true
- ; here's the continue_handler for the leftmost application of f
- larger_computation
- ; here's the abort_handler
- larger_computation in
+ f h z (\z. t f z continue_handler abort_handler) abort_handler
+
+ now `extract_head` should return the leftmost head directly, using its `abort_handler`:
+
let extract_head = \lst larger_computation. lst
- ; here's our f
- (\hd sofar continue_handler abort_handler. continue_handler hd)
- ; here's our z
+ (\hd sofar continue_handler abort_handler. abort_handler hd)
junk
- ; here's the continue_handler for the leftmost application of f
larger_computation
- ; here's the abort_handler
- larger_computation in
- let extract_tail = ; left as exercise
- ;; for real efficiency, it'd be nice to fuse the apparatus developed
- ;; in these v5 lists with the ideas from the v4 lists, above
- ;; but that also is left as an exercise
+ larger_computation
- These functions are used like this:
+3. To extract tails efficiently, too, it'd be nice to fuse the apparatus developed
+ in these v5 lists with the ideas from the v4 lists, above.
+ But that also is left as an exercise.
- 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`.
+#Implementing trees#
- 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.
+In [[Assignment3]] we proposed a very ad-hoc-ish implementation of trees.
-<!-- (Silly [cultural reference](http://www.newgrounds.com/portal/view/33440).) -->
+Think about how you'd implement them in a more principled way. You could
+use any of the version 1 -- version 5 implementation of lists as a model.
+
+To keep things simple, we'll stick to binary trees. A node will either be a
+*leaf* of the tree, or it will have exactly two children.
+
+There are two kinds of trees to think about. In one sort of tree, it's only
+the tree's leaves that are labeled:
+
+ .
+ / \
+ . 3
+ / \
+ 1 2
+
+Linguists often use trees of this sort. The inner, non-leaf nodes of the
+tree do have associated values. But what values they are can be determined from
+the structure of the tree and the values of the node's left and right children.
+So the inner node doesn't need its own independent label.
+
+In another sort of tree, the tree's inner nodes are also labeled:
+
+ 4
+ / \
+ 2 5
+ / \
+ 1 3
+
+When you want to efficiently arrange an ordered collection, so that it's
+easy to do a binary search through it, this is the way you usually structure
+your data.
+
+These latter sorts of trees can helpfully be thought of as ones where
+*only* the inner nodes are labeled. Leaves can be thought of as special,
+dead-end branches with no label:
+
+ .4.
+ / \
+ 2 5
+ / \ / \
+ 1 3 x x
+ / \ / \
+ x x x x
+
+In our earlier discussion of lists, we said they could be thought of as
+data structures of the form:
+
+ Empty_list | Non_empty_list (its_head, its_tail)
+
+And that could in turn be implemented in v2 form as:
+
+ the_list (\head tail. non_empty_handler) empty_handler
+
+Similarly, the leaf-labeled tree:
+
+ .
+ / \
+ . 3
+ / \
+ 1 2
+
+can be thought of as a data structure of the form:
+
+ Leaf (its_label) | Non_leaf (its_left_subtree, its_right_subtree)
+
+and that could be implemented in v2 form as:
+
+ the_tree (\left right. non_leaf_handler) (\label. leaf_handler)
+
+And the node-labeled tree:
+
+ .4.
+ / \
+ 2 5
+ / \ / \
+ 1 3 x x
+ / \ / \
+ x x x x
+
+can be thought of as a data structure of the form:
+
+ Leaf | Non_leaf (its_left_subtree, its_label, its_right_subtree)
+
+and that could be implemented in v2 form as:
+
+ the_tree (\left label right. non_leaf_handler) leaf_result
+
+
+What would correspond to "folding" a function `f` and base value `z` over a
+tree? Well, if it's an empty tree:
+
+ x
+
+we should presumably get back `z`. And if it's a simple, non-empty tree:
+
+ 1
+ / \
+ x x
+
+we should expect something like `f z 1 z`, or `f <result of folding f and z
+over left subtree> label_of_this_node <result of folding f and z over right
+subtree>`. (It's not important what order we say `f` has to take its arguments
+in.)
+
+A v3-style implementation of node-labeled trees, then, might be:
+
+ let empty_tree = \f z. z in
+ let make_tree = \left label right. \f z. f (left f z) label (right f z) in
+ ...
+
+Think about how you might implement other tree operations, such as getting
+the label of the root (topmost node) of a tree; extracting the left subtree of
+a node; and so on.
+
+Think about different ways you might implement leaf-labeled trees.
+
+If you had one tree and wanted to make a larger tree out of it, adding in a
+new element, how would you do that?
+
+When using trees to represent linguistic structures, one doesn't have
+latitude about *how* to build a larger tree. The linguistic structure you're
+trying to represent will determine where the new element should be placed, and
+where the previous tree should be placed.
+
+However, when using trees as a computational tool, one usually does have
+latitude about how to structure a larger tree---in the same way that we had the
+freedom to implement our sets with lists whose members were just appended in
+the order we built the set up, or instead with lists whose members were ordered
+numerically.
+
+When building a new tree, one strategy for where to put the new element and
+where to put the existing tree would be to always lean towards a certain side.
+For instance, to add the element `2` to the tree:
+
+ 1
+ / \
+ x x
+
+we might construct the following tree:
+
+ 1
+ / \
+ x 2
+ / \
+ x x
+
+or perhaps we'd do it like this instead:
+
+ 2
+ / \
+ x 1
+ / \
+ x x
+
+However, if we always leaned to the right side in this way, then the tree
+would get deeper and deeper on that side, but never on the left:
+
+ 1
+ / \
+ x 2
+ / \
+ x 3
+ / \
+ x 4
+ / \
+ x 5
+ / \
+ x x
+
+and that wouldn't be so useful if you were using the tree as an arrangement
+to enable *binary searches* over the elements it holds. For that, you'd prefer
+the tree to be relatively "balanced", like this:
+
+ .4.
+ / \
+ 2 5
+ / \ / \
+ 1 3 x x
+ / \ / \
+ x x x x
+
+Do you have any ideas about how you might efficiently keep the new trees
+you're building pretty "balanced" in this way?
- 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).
+This is a large topic in computer science. There's no need for you to learn
+the various strategies that they've developed for doing this. But
+thinking in broad brush-strokes about what strategies might be promising will
+help strengthen your understanding of trees, and useful ways to implement them
+in a purely functional setting like the lambda calculus.
-5. Implementing (self-balancing) trees
- more...