1 1. How would you define an operation to reverse a list? (Don't peek at the
2 [[lambda_library]]! Try to figure it out on your own.) Choose whichever
3 implementation of list you like. Even then, there are various strategies you
7 2. An advantage of the v3 lists and v3 (aka "Church") numerals is that they
8 have a recursive capacity built into their skeleton. So for many natural
9 operations on them, you won't need to use a fixed point combinator. Why is
10 that an advantage? Well, if you use a fixed point combinator, then the terms
12 won't be strongly normalizing: whether their reduction stops at a normal form
13 will depend on what evaluation order you use. Our online [[lambda evaluator]]
14 uses normal-order reduction, so it finds a normal form if there's one to be
15 had. But if you want to build lambda terms in, say, Scheme, and you wanted to
16 roll your own recursion as we've been doing, rather than relying on Scheme's
17 native `let rec` or `define`, then you can't use the fixed-point combinators
18 `Y` or <code>Θ</code>. Expressions using them will have non-terminating
19 reductions, with Scheme's eager/call-by-value strategy. There are other
20 fixed-point combinators you can use with Scheme (in the [week 3 notes](/week3/#index7h2) they
21 were <code>Y′</code> and <code>Θ′</code>. But even with
22 them, evaluation order still matters: for some (admittedly unusual)
23 evaluation strategies, expressions using them will also be non-terminating.
25 The fixed-point combinators may be the conceptual stars. They are cool and
26 mathematically elegant. But for efficiency and implementation elegance, it's
27 best to know how to do as much as you can without them. (Also, that knowledge
28 could carry over to settings where the fixed point combinators are in
29 principle unavailable.)
31 This is why the v3 lists and numbers are so lovely. However, one disadvantage
32 to them is that it's relatively inefficient to extract a list's tail, or get a
33 number's predecessor. To get the tail of the list `[a;b;c;d;e]`, one will
34 basically be performing some operation that builds up the tail afresh: at
35 different stages, one will have built up `[e]`, then `[d;e]`, then `[c;d;e]`, and
36 finally `[b;c;d;e]`. With short lists, this is no problem, but with longer lists
37 it takes longer and longer. And it may waste more of your computer's memory
38 than you'd like. Similarly for obtaining a number's predecessor.
40 The v1 lists and numbers on the other hand, had the tail and the predecessor
41 right there as an element, easy for the taking. The problem was just that the
42 v1 lists and numbers didn't have recursive capacity built into them, in the
43 way the v3 implementations do.
45 A clever approach would marry these two strategies.
47 Version 3 makes the list `[a;b;c;d;e]` look like this:
49 \f z. f a (f b (f c (f d (f e z))))
53 \f z. f a <the result of folding f and z over the tail>
55 Instead we could make it look like this:
57 \f z. f a <the tail itself> <the result of folding f and z over the tail>
59 That is, now `f` is a function expecting *three* arguments: the head of the
60 current list, the tail of the current list, and the result of continuing to
61 fold `f` over the tail, with a given base value `z`.
63 Call this a **version 4** list. The empty list can be the same as in v3:
65 <pre><code>empty ≡ \f z. z</code></pre>
67 The list constructor would be:
69 <pre><code>make_list ≡ \h t. \f z. f h t (t f z)</code></pre>
71 It differs from the version 3 `make_list` only in adding the extra argument
72 `t` to the new, outer application of `f`.
74 Similarly, `five` as a v3 or Church numeral looks like this:
76 \s z. s (s (s (s (s z))))
80 \s z. s <the result of applying s to z (pred 5)-many times>
82 Instead we could make it look like this:
84 \s z. s <pred 5> <the result of applying s to z (pred 5)-many times>
86 That is, now `s` is a function expecting *two* arguments: the predecessor of the
87 current number, and the result of continuing to apply `s` to the base value `z`
88 predecessor-many times.
90 Jim had the pleasure of "inventing" these implementations himself. However,
91 unsurprisingly, he wasn't the first to do so. See for example [Oleg's report
92 on P-numerals](http://okmij.org/ftp/Computation/lambda-calc.html#p-numerals).
98 You're now already in a position to implement sets: that is, collections with
99 no intrinsic order where elements can occur at most once. Like lists, we'll
100 understand the basic set structures to be *type-homogenous*. So you might have
101 a set of integers, or you might have a set of pairs of integers, but you
102 wouldn't have a set that mixed both types of elements. Something *like* the
103 last option is also achievable, but it's more difficult, and we won't pursue it
104 now. In fact, we won't talk about sets of pairs, either. We'll just talk about
105 sets of integers. The same techniques we discuss here could also be applied to
106 sets of pairs of integers, or sets of triples of booleans, or sets of pairs
107 whose first elements are booleans, and whose second elements are triples of
110 (You're also now in a position to implement *multi*sets: that is, collections
111 with no intrinsic order where elements can occur multiple times: the multiset
112 {a,a} is distinct from the multiset {a}. But we'll leave these as an exercise.)
114 The easiest way to implement sets of integers would just be to use lists. When
115 you "add" a member to a set, you'd get back a list that was either identical to
116 the original list, if the added member already was present in it, or consisted
117 of a new list with the added member prepended to the old list. That is:
119 let empty_set = empty in
120 ; see the library for definitions of any and eq
121 let make_set = \new_member old_set. any (eq new_member) old_set
122 ; if any element in old_set was eq new_member
125 make_list new_member old_set
127 Think about how you'd implement operations like `set_union`,
128 `set_intersection`, and `set_difference` with this implementation of sets.
130 The implementation just described works, and it's the simplest to code.
131 However, it's pretty inefficient. If you had a 100-member set, and you wanted
132 to create a set which had all those 100-members and some possibly new element
133 `e`, you might need to check all 100 members to see if they're equal to `e`
134 before concluding they're not, and returning the new list. And comparing for
135 numeric equality is a moderately expensive operation, in the first place.
137 (You might say, well, what's the harm in just prepending `e` to the list even
138 if it already occurs later in the list. The answer is, if you don't keep track
139 of things like this, it will likely mess up your implementations of
140 `set_difference` and so on. You'll have to do the book-keeping for duplicates
141 at some point in your code. It goes much more smoothly if you plan this from
144 How might we make the implementation more efficient? Well, the *semantics* of
145 sets says that they have no intrinsic order. That means, there's no difference
146 between the set {a,b} and the set {b,a}; whereas there is a difference between
147 the *list* `[a;b]` and the list `[b;a]`. But this semantic point can be respected
148 even if we *implement* sets with something ordered, like list---as we're
149 already doing. And we might *exploit* the intrinsic order of lists to make our
150 implementation of sets more efficient.
152 What we could do is arrange it so that a list that implements a set always
153 keeps in elements in some specified order. To do this, there'd have *to be*
154 some way to order its elements. Since we're talking now about sets of numbers,
155 that's easy. (If we were talking about sets of pairs of numbers, we'd use
156 "lexicographic" ordering, where `(a,b) < (c,d)` iff `a < c or (a == c and b <
159 So, if we were searching the list that implements some set to see if the number
160 `5` belonged to it, once we get to elements in the list that are larger than `5`,
161 we can stop. If we haven't found `5` already, we know it's not in the rest of the
164 This is an improvement, but it's still a "linear" search through the list.
165 There are even more efficient methods, which employ "binary" searching. They'd
166 represent the set in such a way that you could quickly determine whether some
167 element fell in one half, call it the left half, of the structure that
168 implements the set, if it belonged to the set at all. Or that it fell in the
169 right half, it it belonged to the set at all. And then the same sort of
170 determination could be made for whichever half you were directed to. And then
171 for whichever quarter you were directed to next. And so on. Until you either
172 found the element or exhausted the structure and could then conclude that the
173 element in question was not part of the set. These sorts of structures are done
174 using **binary trees** (see below).
177 4. **Aborting a search through a list**
179 We said that the sorted-list implementation of a set was more efficient than
180 the unsorted-list implementation, because as you were searching through the
181 list, you could come to a point where you knew the element wasn't going to be
182 found. So you wouldn't have to continue the search.
184 If your implementation of lists was, say v1 lists plus the Y-combinator, then
185 this is exactly right. When you get to a point where you know the answer, you
186 can just deliver that answer, and not branch into any further recursion. If
187 you've got the right evaluation strategy in place, everything will work out
190 But what if you're using v3 lists? What options would you have then for
193 Well, suppose we're searching through the list `[5;4;3;2;1]` to see if it
194 contains the number `3`. The expression which represents this search would have
195 something like the following form:
197 ..................<eq? 1 3> ~~>
198 .................. false ~~>
199 .............<eq? 2 3> ~~>
200 ............. false ~~>
201 .........<eq? 3 3> ~~>
205 Of course, whether those reductions actually followed in that order would
206 depend on what reduction strategy was in place. But the result of folding the
207 search function over the part of the list whose head is `3` and whose tail is `[2;
208 1]` will *semantically* depend on the result of applying that function to the
209 more rightmost pieces of the list, too, regardless of what order the reduction
210 is computed by. Conceptually, it will be easiest if we think of the reduction
211 happening in the order displayed above.
213 Well, once we've found a match between our sought number `3` and some member of
214 the list, we'd like to avoid any further unnecessary computations and just
215 deliver the answer `true` as "quickly" or directly as possible to the larger
216 computation in which the search was embedded.
218 With a Y-combinator based search, as we said, we could do this by just not
219 following a recursion branch.
221 But with the v3 lists, the fold is "pre-programmed" to continue over the whole
222 list. There is no way for us to bail out of applying the search function to the
223 parts of the list that have head `4` and head `5`, too.
225 We *can* avoid *some* unneccessary computation. The search function can detect
226 that the result we've accumulated so far during the fold is now `true`, so we
227 don't need to bother comparing `4` or `5` to `3` for equality. That will simplify the
228 computation to some degree, since as we said, numerical comparison in the
229 system we're working in is moderately expensive.
231 However, we're still going to have to traverse the remainder of the list. That
232 `true` result will have to be passed along all the way to the leftmost head of
233 the list. Only then can we deliver it to the larger computation in which the
236 It would be better if there were some way to "abort" the list traversal. If,
237 having found the element we're looking for (or having determined that the
238 element isn't going to be found), we could just immediately stop traversing the
239 list with our answer. **Continuations** will turn out to let us do that.
241 We won't try yet to fully exploit the terrible power of continuations. But
242 there's a way that we can gain their benefits here locally, without yet having
243 a fully general machinery or understanding of what's going on.
245 The key is to recall how our implementations of booleans and pairs worked.
246 Remember that with pairs, we supply the pair "handler" to the pair as *an
247 argument*, rather than the other way around:
255 to get the first element of the pair. Of course you can lift that if you want:
257 <pre><code>extract_fst ≡ \pair. pair (\x y. x)</code></pre>
259 but at a lower level, the pair is still accepting its handler as an argument,
260 rather than the handler taking the pair as an argument. (The handler gets *the
261 pair's elements*, not the pair itself, as arguments.)
263 > *Terminology*: we'll try to use names of the form `get_foo` for handlers, and
264 names of the form `extract_foo` for lifted versions of them, that accept the
265 lists (or whatever data structure we're working with) as arguments. But we may
268 The v2 implementation of lists followed a similar strategy:
270 v2list (\h t. do_something_with_h_and_t) result_if_empty
272 If the `v2list` here is not empty, then this will reduce to the result of
273 supplying the list's head and tail to the handler `(\h t.
274 do_something_with_h_and_t)`.
276 Now, what we've been imagining ourselves doing with the search through the v3
277 list is something like this:
280 larger_computation (search_through_the_list_for_3) other_arguments
282 That is, the result of our search is supplied as an argument (perhaps together
283 with other arguments) to the "larger computation". Without knowing the
284 evaluation order/reduction strategy, we can't say whether the search is
285 evaluated before or after it's substituted into the larger computation. But
286 semantically, the search is the argument and the larger computation is the
287 function to which it's supplied.
289 What if, instead, we did the same kind of thing we did with pairs and v2
290 lists? That is, what if we made the larger computation a "handler" that we
291 passed as an argument to the search?
293 the_search (\search_result. larger_computation search_result other_arguments)
295 What's the advantage of that, you say. Other than to show off how cleverly
298 Well, think about it. Think about the difficulty we were having aborting the
299 search. Does this switch-around offer us anything useful?
303 What if the way we implemented the search procedure looked something like this?
305 At a given stage in the search, we wouldn't just apply some function `f` to the
306 head at this stage and the result accumulated so far (from folding the same
307 function, and a base value, to the tail at this stage)...and then pass the result
308 of that application to the embedding, more leftward computation.
310 We'd *instead* give `f` a "handler" that expects the result of the current
311 stage *as an argument*, and then evaluates to what you'd get by passing that
312 result leftwards up the list, as before.
314 Why would we do that, you say? Just more flamboyant lifting?
316 Well, no, there's a real point here. If we give the function a "handler" that
317 encodes the normal continuation of the fold leftwards through the list, we can
318 also give it other "handlers" too. For example, we can also give it the underlined handler:
321 the_search (\search_result. larger_computation search_result other_arguments)
322 ------------------------------------------------------------------
324 This "handler" encodes the search's having finished, and delivering a final
325 answer to whatever else you wanted your program to do with the result of the
326 search. If you like, at any stage in the search you might just give an argument
327 to *this* handler, instead of giving an argument to the handler that continues
328 the list traversal leftwards. Semantically, this would amount to *aborting* the
329 list traversal! (As we've said before, whether the rest of the list traversal
330 really gets evaluated will depend on what evaluation order is in place. But
331 semantically we'll have avoided it. Our larger computation won't depend on the
332 rest of the list traversal having been computed.)
334 Do you have the basic idea? Think about how you'd implement it. A good
335 understanding of the v2 lists will give you a helpful model.
337 In broad outline, a single stage of the search would look like before, except
338 now f would receive two extra, "handler" arguments.
340 f 3 <result of folding f and z over [2; 1]> <handler to continue folding leftwards> <handler to abort the traversal>
342 `f`'s job would be to check whether `3` matches the element we're searching for
343 (here also `3`), and if it does, just evaluate to the result of passing `true` to
344 the abort handler. If it doesn't, then evaluate to the result of passing
345 `false` to the continue-leftwards handler.
347 In this case, `f` wouldn't need to consult the result of folding `f` and `z` over `[2;
348 1]`, since if we had found the element `3` in more rightward positions of the
349 list, we'd have called the abort handler and this application of `f` to `3` etc
350 would never be needed. However, in other applications the result of folding `f`
351 and `z` over the more rightward parts of the list would be needed. Consider if
352 you were trying to multiply all the elements of the list, and were going to
353 abort (with the result `0`) if you came across any element in the list that was
354 zero. If you didn't abort, you'd need to know what the more rightward elements
355 of the list multiplied to, because that would affect the answer you passed
356 along to the continue-leftwards handler.
358 A **version 5** list encodes the kind of fold operation we're envisaging here, in
359 the same way that v3 (and v4) lists encoded the simpler fold operation.
360 Roughly, the list `[5;4;3;2;1]` would look like this:
363 \f z continue_leftwards_handler abort_handler.
364 <fold f and z over [4;3;2;1]>
365 (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 continue_leftwards_handler abort_handler)
368 ; or, expanding the fold over [4;3;2;1]:
370 \f z continue_leftwards_handler abort_handler.
371 (\continue_leftwards_handler abort_handler.
372 <fold f and z over [3;2;1]>
373 (\result_of_fold_over_321. f 4 result_of_fold_over_321 continue_leftwards_handler abort_handler)
376 (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 continue_leftwards_handler abort_handler)
381 Remarks: the `larger_computation` handler should be supplied as both the
382 `continue_leftwards_handler` and the `abort_handler` for the leftmost
383 application, where the head `5` is supplied to `f`; because the result of this
384 application should be passed to the larger computation, whether it's a "fall
385 off the left end of the list" result or it's a "I'm finished, possibly early"
386 result. The `larger_computation` handler also then gets passed to the next
387 rightmost stage, where the head `4` is supplied to `f`, as the `abort_handler` to
388 use if that stage decides it has an early answer.
390 Finally, notice that we don't have the result of applying `f` to `4` etc given as
391 an argument to the application of `f` to `5` etc. Instead, we pass
393 (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 <one_handler> <another_handler>)
395 *to* the application of `f` to `4` as its "continue" handler. The application of `f`
396 to `4` can decide whether this handler, or the other, "abort" handler, should be
397 given an argument and constitute its result.
400 I'll say once again: we're using temporally-loaded vocabulary throughout this,
401 but really all we're in a position to mean by that are claims about the result
402 of the complex expression semantically depending only on this, not on that. A
403 demon evaluator who custom-picked the evaluation order to make things maximally
404 bad for you could ensure that all the semantically unnecessary computations got
405 evaluated anyway. We don't have any way to prevent that. Later,
406 we'll see ways to *semantically guarantee* one evaluation order rather than
407 another. Though even then the demonic evaluation-order-chooser could make it
408 take unnecessarily long to compute the semantically guaranteed result. Of
409 course, in any real computing environment you'll know you're dealing with a
410 fixed evaluation order and you'll be able to program efficiently around that.
412 In detail, then, here's what our v5 lists will look like:
414 let empty = \f z continue_handler abort_handler. continue_handler z in
415 let make_list = \h t. \f z continue_handler abort_handler.
416 t f z (\sofar. f h sofar continue_handler abort_handler) abort_handler in
417 let isempty = \lst larger_computation. lst
419 (\hd sofar continue_handler abort_handler. abort_handler false)
422 ; here's the continue_handler for the leftmost application of f
424 ; here's the abort_handler
425 larger_computation in
426 let extract_head = \lst larger_computation. lst
428 (\hd sofar continue_handler abort_handler. continue_handler hd)
431 ; here's the continue_handler for the leftmost application of f
433 ; here's the abort_handler
434 larger_computation in
435 let extract_tail = ; left as exercise
437 These functions are used like this:
439 let my_list = make_list a (make_list b (make_list c empty) in
440 extract_head my_list larger_computation
442 If you just want to see `my_list`'s head, the use `I` as the
443 `larger_computation`.
445 What we've done here does take some work to follow. But it should be within
446 your reach. And once you have followed it, you'll be well on your way to
447 appreciating the full terrible power of continuations.
449 <!-- (Silly [cultural reference](http://www.newgrounds.com/portal/view/33440).) -->
451 Of course, like everything elegant and exciting in this seminar, [Oleg
452 discusses it in much more
453 detail](http://okmij.org/ftp/Streams.html#enumerator-stream).
457 1. The technique deployed here, and in the v2 lists, and in our implementations
458 of pairs and booleans, is known as **continuation-passing style** programming.
460 2. We're still building the list as a right fold, so in a sense the
461 application of `f` to the leftmost element `5` is "outermost". However,
462 this "outermost" application is getting lifted, and passed as a *handler*
463 to the next right application. Which is in turn getting lifted, and
464 passed to its next right application, and so on. So if you
465 trace the evaluation of the `extract_head` function to the list `[5;4;3;2;1]`,
466 you'll see `1` gets passed as a "this is the head sofar" answer to its
467 `continue_handler`; then that answer is discarded and `2` is
468 passed as a "this is the head sofar" answer to *its* `continue_handler`,
469 and so on. All those steps have to be evaluated to finally get the result
470 that `5` is the outer/leftmost head of the list. That's not an efficient way
471 to get the leftmost head.
473 We could improve this by building lists as left folds when implementing them
474 as continuation-passing style folds. We'd just replace above:
476 let make_list = \h t. \f z continue_handler abort_handler.
477 f h z (\z. t f z continue_handler abort_handler) abort_handler
479 now `extract_head` should return the leftmost head directly, using its `abort_handler`:
481 let extract_head = \lst larger_computation. lst
482 (\hd sofar continue_handler abort_handler. abort_handler hd)
487 3. To extract tails efficiently, too, it'd be nice to fuse the apparatus developed
488 in these v5 lists with the ideas from the v4 lists, above.
489 But that also is left as an exercise.
492 5. Implementing (self-balancing) trees