rename
[lambda.git] / topics / week12_abortable_traversals.mdwn
1 #Aborting a search through a list#
2
3 We said that the sorted-list implementation of a set was more efficient than
4 the unsorted-list implementation, because as you were searching through the
5 list, you could come to a point where you knew the element wasn't going to be
6 found. So you wouldn't have to continue the search.
7
8 If your implementation of lists was, say v1 lists plus the Y-combinator, then
9 this is exactly right. When you get to a point where you know the answer, you
10 can just deliver that answer, and not branch into any further recursion. If
11 you've got the right evaluation strategy in place, everything will work out
12 fine.
13
14 But what if we wanted to use v3 lists instead?
15
16 >       Why would we want to do that? The advantage of the v3 lists and v3 (aka
17 "Church") numerals is that they have their recursive capacity built into their
18 very bones. So for many natural operations on them, you won't need to use a fixed
19 point combinator.
20
21 >       Why is that an advantage? Well, if you use a fixed point combinator, then
22 the terms you get won't be strongly normalizing: whether their reduction stops
23 at a normal form will depend on what evaluation order you use. Our online
24 [[lambda evaluator]] uses normal-order reduction, so it finds a normal form if
25 there's one to be had. But if you want to build lambda terms in, say, Scheme,
26 and you wanted to roll your own recursion as we've been doing, rather than
27 relying on Scheme's native `let rec` or `define`, then you can't use the
28 fixed-point combinators `Y` or <code>&Theta;</code>. Expressions using them
29 will have non-terminating reductions, with Scheme's eager/call-by-value
30 strategy. There are other fixed-point combinators you can use with Scheme (in
31 the [week 3 notes](/week3/#index7h2) they were <code>Y&prime;</code> and
32 <code>&Theta;&prime;</code>. But even with them, evaluation order still
33 matters: for some (admittedly unusual) evaluation strategies, expressions using
34 them will also be non-terminating.
35
36 >       The fixed-point combinators may be the conceptual stars. They are cool and
37 mathematically elegant. But for efficiency and implementation elegance, it's
38 best to know how to do as much as you can without them. (Also, that knowledge
39 could carry over to settings where the fixed point combinators are in principle
40 unavailable.)
41
42
43 So again, what if we're using v3 lists? What options would we have then for
44 aborting a search or list traversal before it runs to completion?
45
46 Suppose we're searching through the list `[5;4;3;2;1]` to see if it
47 contains the number `3`. The expression which represents this search would have
48 something like the following form:
49
50         ..................<eq? 1 3>  ~~>
51         .................. false     ~~>
52         .............<eq? 2 3>       ~~>
53         ............. false          ~~>
54         .........<eq? 3 3>           ~~>
55         ......... true               ~~>
56         ?
57
58 Of course, whether those reductions actually followed in that order would
59 depend on what reduction strategy was in place. But the result of folding the
60 search function over the part of the list whose head is `3` and whose tail is `[2;
61 1]` will *semantically* depend on the result of applying that function to the
62 more rightmost pieces of the list, too, regardless of what order the reduction
63 is computed by. Conceptually, it will be easiest if we think of the reduction
64 happening in the order displayed above.
65
66 Once we've found a match between our sought number `3` and some member of
67 the list, we'd like to avoid any further unnecessary computations and just
68 deliver the answer `true` as "quickly" or directly as possible to the larger
69 computation in which the search was embedded.
70
71 With a Y-combinator based search, as we said, we could do this by just not
72 following a recursion branch.
73
74 But with the v3 lists, the fold is "pre-programmed" to continue over the whole
75 list. There is no way for us to bail out of applying the search function to the
76 parts of the list that have head `4` and head `5`, too.
77
78 We *can* avoid *some* unneccessary computation. The search function can detect
79 that the result we've accumulated so far during the fold is now `true`, so we
80 don't need to bother comparing `4` or `5` to `3` for equality. That will simplify the
81 computation to some degree, since as we said, numerical comparison in the
82 system we're working in is moderately expensive.
83
84 However, we're still going to have to traverse the remainder of the list. That
85 `true` result will have to be passed along all the way to the leftmost head of
86 the list. Only then can we deliver it to the larger computation in which the
87 search was embedded.
88
89 It would be better if there were some way to "abort" the list traversal. If,
90 having found the element we're looking for (or having determined that the
91 element isn't going to be found), we could just immediately stop traversing the
92 list with our answer. **Continuations** will turn out to let us do that.
93
94 We won't try yet to fully exploit the terrible power of continuations. But
95 there's a way that we can gain their benefits here locally, without yet having
96 a fully general machinery or understanding of what's going on.
97
98 The key is to recall how our implementations of booleans and pairs worked.
99 Remember that with pairs, we supply the pair "handler" to the pair as *an
100 argument*, rather than the other way around:
101
102         pair (\x y. add x y)
103
104 or:
105
106         pair (\x y. x)
107
108 to get the first element of the pair. Of course you can lift that if you want:
109
110 <pre><code>extract_fst &equiv; \pair. pair (\x y. x)</code></pre>
111
112 but at a lower level, the pair is still accepting its handler as an argument,
113 rather than the handler taking the pair as an argument. (The handler gets *the
114 pair's elements*, not the pair itself, as arguments.)
115
116 >       *Terminology*: we'll try to use names of the form `get_foo` for handlers, and
117 names of the form `extract_foo` for lifted versions of them, that accept the
118 lists (or whatever data structure we're working with) as arguments. But we may
119 sometimes forget.
120
121 The v2 implementation of lists followed a similar strategy:
122
123         v2list (\h t. do_something_with_h_and_t) result_if_empty
124
125 If the `v2list` here is not empty, then this will reduce to the result of
126 supplying the list's head and tail to the handler `(\h t.
127 do_something_with_h_and_t)`.
128
129 Now, what we've been imagining ourselves doing with the search through the v3
130 list is something like this:
131
132
133         larger_computation (search_through_the_list_for_3) other_arguments
134
135 That is, the result of our search is supplied as an argument (perhaps together
136 with other arguments) to the "larger computation". Without knowing the
137 evaluation order/reduction strategy, we can't say whether the search is
138 evaluated before or after it's substituted into the larger computation. But
139 semantically, the search is the argument and the larger computation is the
140 function to which it's supplied.
141
142 What if, instead, we did the same kind of thing we did with pairs and v2
143 lists? That is, what if we made the larger computation a "handler" that we
144 passed as an argument to the search?
145
146         the_search (\search_result. larger_computation search_result other_arguments)
147
148 What's the advantage of that, you say. Other than to show off how cleverly
149 you can lift.
150
151 Well, think about it. Think about the difficulty we were having aborting the
152 search. Does this switch-around offer us anything useful?
153
154 It could.
155
156 What if the way we implemented the search procedure looked something like this?
157
158 At a given stage in the search, we wouldn't just apply some function `f` to the
159 head at this stage and the result accumulated so far (from folding the same
160 function, and a base value, to the tail at this stage)...and then pass the result
161 of that application to the embedding, more leftward computation.
162
163 We'd *instead* give `f` a "handler" that expects the result of the current
164 stage *as an argument*, and then evaluates to what you'd get by passing that
165 result leftwards up the list, as before. 
166
167 Why would we do that, you say? Just more flamboyant lifting?
168
169 Well, no, there's a real point here. If we give the function a "handler" that
170 encodes the normal continuation of the fold leftwards through the list, we can
171 also give it other "handlers" too. For example, we can also give it the underlined handler:
172
173
174         the_search (\search_result. larger_computation search_result other_arguments)
175                            ------------------------------------------------------------------
176
177 This "handler" encodes the search's having finished, and delivering a final
178 answer to whatever else you wanted your program to do with the result of the
179 search. If you like, at any stage in the search you might just give an argument
180 to *this* handler, instead of giving an argument to the handler that continues
181 the list traversal leftwards. Semantically, this would amount to *aborting* the
182 list traversal! (As we've said before, whether the rest of the list traversal
183 really gets evaluated will depend on what evaluation order is in place. But
184 semantically we'll have avoided it. Our larger computation  won't depend on the
185 rest of the list traversal having been computed.)
186
187 Do you have the basic idea? Think about how you'd implement it. A good
188 understanding of the v2 lists will give you a helpful model.
189
190 In broad outline, a single stage of the search would look like before, except
191 now `f` would receive two extra, "handler" arguments. We'll reserve the name `f` for the original fold function, and use `f2` for the function that accepts two additional handler arguments. To get the general idea, you can regard these as interchangeable. If the extra precision might help, then you can pay attention to when we're talking about the handler-taking `f2` or the original `f`. You'll only be *supplying* the `f2` function; the idea will be that the behavior of the original `f` will be implicitly encoded in `f2`'s behavior.
192
193         f2 3 <sofar value that would have resulted from folding f and z over [2; 1]> <handler to continue folding leftwards> <handler to abort the traversal>
194
195 `f2`'s job would be to check whether `3` matches the element we're searching for
196 (here also `3`), and if it does, just evaluate to the result of passing `true` to
197 the abort handler. If it doesn't, then evaluate to the result of passing
198 `false` to the continue-leftwards handler.
199
200 In this case, `f2` wouldn't need to consult the result of folding `f` and `z`
201 over `[2; 1]`, since if we had found the element `3` in more rightward
202 positions of the list, we'd have called the abort handler and this application
203 of `f2` to `3` etc would never be needed. However, in other applications the
204 result of folding `f` and `z` over the more rightward parts of the list would
205 be needed. Consider if you were trying to multiply all the elements of the
206 list, and were going to abort (with the result `0`) if you came across any
207 element in the list that was zero. If you didn't abort, you'd need to know what
208 the more rightward elements of the list multiplied to, because that would
209 affect the answer you passed along to the continue-leftwards handler.
210
211 A **version 5** list encodes the kind of fold operation we're envisaging here,
212 in the same way that v3 (and [v4](/advanced_lambda/#index1h1)) lists encoded
213 the simpler fold operation. Roughly, the list `[5;4;3;2;1]` would look like
214 this:
215
216
217         \f2 z continue_leftwards_handler abort_handler.
218                 <fold f2 and z over [4;3;2;1]>
219                 (\result_of_folding_over_4321. f2 5 result_of_folding_over_4321  continue_leftwards_handler abort_handler)
220                 abort_handler
221
222         ; or, expanding the fold over [4;3;2;1]:
223
224         \f2 z continue_leftwards_handler abort_handler.
225                 (\continue_leftwards_handler abort_handler.
226                         <fold f2 and z over [3;2;1]>
227                         (\result_of_folding_over_321. f2 4 result_of_folding_over_321 continue_leftwards_handler abort_handler)
228                         abort_handler
229                 )
230                 (\result_of_folding_over_4321. f2 5 result_of_folding_over_4321  continue_leftwards_handler abort_handler)
231                 abort_handler
232
233         ; and so on
234         
235 Remarks: the `larger_computation` handler should be supplied as both the
236 `continue_leftwards_handler` and the `abort_handler` for the leftmost
237 application, where the head `5` is supplied to `f2`; because the result of this
238 application should be passed to the larger computation, whether it's a "fall
239 off the left end of the list" result or it's a "I'm finished, possibly early"
240 result. The `larger_computation` handler also then gets passed to the next
241 rightmost stage, where the head `4` is supplied to `f2`, as the `abort_handler` to
242 use if that stage decides it has an early answer.
243
244 Finally, notice that we're not supplying the application of `f2` to `4` etc as an argument to the application of `f2` to `5` etc---at least, not directly. Instead, we pass
245
246         (\result_of_folding_over_4321. f2 5 result_of_folding_over_4321 <one_handler> <another_handler>)
247
248 *to* the application of `f2` to `4` as its "continue" handler. The application of `f2`
249 to `4` can decide whether this handler, or the other, "abort" handler, should be
250 given an argument and constitute its result.
251
252
253 I'll say once again: we're using temporally-loaded vocabulary throughout this,
254 but really all we're in a position to mean by that are claims about the result
255 of the complex expression semantically depending only on this, not on that. A
256 demon evaluator who custom-picked the evaluation order to make things maximally
257 bad for you could ensure that all the semantically unnecessary computations got
258 evaluated anyway. We don't yet know any way to prevent that. Later, we'll see
259 ways to *guarantee* one evaluation order rather than another. Of
260 course, in any real computing environment you'll know in advance that you're
261 dealing with a fixed evaluation order and you'll be able to program efficiently
262 around that.
263
264 In detail, then, here's what our v5 lists will look like:
265
266         let empty = \f2 z continue_handler abort_handler. continue_handler z  in
267         let make_list = \h t. \f2 z continue_handler abort_handler.
268                 t f2 z (\sofar. f2 h sofar continue_handler abort_handler) abort_handler  in
269         let isempty = \lst larger_computation. lst
270                         ; here's our f2
271                         (\hd sofar continue_handler abort_handler. abort_handler false)
272                         ; here's our z
273                         true
274                         ; here's the continue_handler for the leftmost application of f2
275                         larger_computation
276                         ; here's the abort_handler
277                         larger_computation  in
278         let extract_head = \lst larger_computation. lst
279                         ; here's our f2
280                         (\hd sofar continue_handler abort_handler. continue_handler hd)
281                         ; here's our z
282                         junk
283                         ; here's the continue_handler for the leftmost application of f2
284                         larger_computation
285                         ; here's the abort_handler
286                         larger_computation  in
287         let extract_tail = ; left as exercise
288
289 These functions are used like this:
290
291         let my_list = make_list a (make_list b (make_list c empty) in
292         extract_head my_list larger_computation
293
294 If you just want to see `my_list`'s head, the use `I` as the
295 `larger_computation`.
296
297 What we've done here does take some work to follow. But it should be within
298 your reach. And once you have followed it, you'll be well on your way to
299 appreciating the full terrible power of continuations.
300
301 <!-- (Silly [cultural reference](http://www.newgrounds.com/portal/view/33440).) -->
302
303 Of course, like everything elegant and exciting in this seminar, [Oleg
304 discusses it in much more
305 detail](http://okmij.org/ftp/Streams.html#enumerator-stream).
306
307 >       *Comments*:
308
309 >       1.      The technique deployed here, and in the v2 lists, and in our
310 >       implementations of pairs and booleans, is known as 
311 >       **continuation-passing style** programming.
312
313 >       2.      We're still building the list as a right fold, so in a sense the
314 >       application of `f2` to the leftmost element `5` is "outermost". However,
315 >       this "outermost" application is getting lifted, and passed as a *handler*
316 >       to the next right application. Which is in turn getting lifted, and
317 >       passed to its next right application, and so on. So if you
318 >       trace the evaluation of the `extract_head` function to the list `[5;4;3;2;1]`,
319 >       you'll see `1` gets passed as a "this is the head sofar" answer to its
320 >       `continue_handler`; then that answer is discarded and `2` is
321 >       passed as a "this is the head sofar" answer to *its* `continue_handler`,
322 >       and so on. All those steps have to be evaluated to finally get the result
323 >       that `5` is the outer/leftmost head of the list. That's not an efficient way
324 >       to get the leftmost head.
325 >       
326 >       We could improve this by building lists as **left folds**. What's that?
327 >       
328 >       Well, the right fold of `f` over a list `[a;b;c;d;e]`, using starting value z, is:
329 >       
330 >                       f a (f b (f c (f d (f e z))))
331 >       
332 >       The left fold on the other hand starts combining `z` with elements from the left. `f z a` is then combined with `b`, and so on:
333 >       
334 >                       f (f (f (f (f z a) b) c) d) e
335 >       
336 >       or, if we preferred the arguments to each `f` flipped:
337 >       
338 >                       f e (f d (f c (f b (f a z))))
339 >       
340 >       Recall we implemented v3 lists as their own right-fold functions. We could
341 >       instead implement lists as their own left-fold functions. To do that with our
342 >       v5 lists, we'd replace above:
343 >       
344 >                       let make_list = \h t. \f2 z continue_handler abort_handler.
345 >                               f2 h z (\z. t f2 z continue_handler abort_handler) abort_handler
346 >       
347 >       Having done that, now `extract_head` can return the leftmost head
348 >       directly, using its `abort_handler`:
349 >       
350 >                       let extract_head = \lst larger_computation. lst
351 >                                       (\hd sofar continue_handler abort_handler. abort_handler hd)
352 >                                       junk
353 >                                       larger_computation
354 >                                       larger_computation
355 >       
356 >       3.      To extract tails efficiently, too, it'd be nice to fuse the apparatus
357 >       developed in these v5 lists with the ideas from 
358 >       [v4](/advanced_lambda/#index1h1) lists. But that is left as an exercise.
359