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