X-Git-Url: http://lambda.jimpryor.net/git/gitweb.cgi?p=lambda.git;a=blobdiff_plain;f=week4.mdwn;h=58d6bc38204bd3bcbf4afabe6d9c4306b82735de;hp=bc154aea1195830c5858a716055ec250c4229a3e;hb=0a9b2c5fb1adfa3b87e95fcbf26ee79d57ae7466;hpb=5fa25870987fce498870f06907422cee6a0bb8b1 diff --git a/week4.mdwn b/week4.mdwn index bc154aea..58d6bc38 100644 --- a/week4.mdwn +++ b/week4.mdwn @@ -139,7 +139,7 @@ start with the `iszero` predicate, and only produce a fresh copy of `prefact` if we are forced to. -#Q. You claimed that the Ackerman function couldn't be implemented using our primitive recursion techniques (such as the techniques that allow us to define addition and multiplication). But you haven't shown that it is possible to define the Ackerman function using full recursion.# +#Q. You claimed that the Ackermann function couldn't be implemented using our primitive recursion techniques (such as the techniques that allow us to define addition and multiplication). But you haven't shown that it is possible to define the Ackermann function using full recursion.# A. OK: @@ -171,7 +171,7 @@ so `A 4 x` is to `A 3 x` as hyper-exponentiation is to exponentiation... * What is it about the variant fixed-point combinators that makes them compatible with a call-by-value evaluation strategy? -* How do you know that the Ackerman function can't be computed +* How do you know that the Ackermann function can't be computed using primitive recursion techniques? * What *exactly* is primitive recursion? @@ -250,7 +250,7 @@ So, if we were searching the list that implements some set to see if the number we can stop. If we haven't found `5` already, we know it's not in the rest of the list either. -*Comment*: This is an improvement, but it's still a "linear" search through the list. +> *Comment*: This is an improvement, but it's still a "linear" search through the list. There are even more efficient methods, which employ "binary" searching. They'd represent the set in such a way that you could quickly determine whether some element fell in one half, call it the left half, of the structure that @@ -276,10 +276,39 @@ 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 +But what if we wanted to use v3 lists instead? + +> Why would we want to do that? The advantage of the v3 lists and v3 (aka +"Church") numerals is that they have their recursive capacity built into their +very bones. So for many natural operations on them, you won't need to use a fixed +point combinator. + +> Why is that an advantage? Well, if you use a fixed point combinator, then +the terms you get won't be strongly normalizing: whether their reduction stops +at a normal form will depend on what evaluation order you use. Our online +[[lambda evaluator]] uses normal-order reduction, so it finds a normal form if +there's one to be had. But if you want to build lambda terms in, say, Scheme, +and you wanted to roll your own recursion as we've been doing, rather than +relying on Scheme's native `let rec` or `define`, then you can't use the +fixed-point combinators `Y` or `Θ`. Expressions using them +will have non-terminating reductions, with Scheme's eager/call-by-value +strategy. There are other fixed-point combinators you can use with Scheme (in +the [week 3 notes](/week3/#index7h2) they were `Y′` and +`Θ′`. But even with them, evaluation order still +matters: for some (admittedly unusual) evaluation strategies, expressions using +them will also be non-terminating. + +> The fixed-point combinators may be the conceptual stars. They are cool and +mathematically elegant. But for efficiency and implementation elegance, it's +best to know how to do as much as you can without them. (Also, that knowledge +could carry over to settings where the fixed point combinators are in principle +unavailable.) + + +So again, what if we're using v3 lists? What options would we have then for +aborting a search or list traversal before it runs to completion? + +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: @@ -299,7 +328,7 @@ 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 +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. @@ -424,64 +453,64 @@ 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. +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. - f 3 + f2 3 -`f`'s job would be to check whether `3` matches the element we're searching for +`f2`'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](/advanced/#index1h1)) 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. - - (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 continue_leftwards_handler abort_handler) +In this case, `f2` 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 `f2` 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](/advanced_lambda/#index1h1)) lists encoded +the simpler fold operation. Roughly, the list `[5;4;3;2;1]` would look like +this: + + + \f2 z continue_leftwards_handler abort_handler. + + (\result_of_folding_over_4321. f2 5 result_of_folding_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. + \f2 z continue_leftwards_handler abort_handler. (\continue_leftwards_handler abort_handler. - - (\result_of_fold_over_321. f 4 result_of_fold_over_321 continue_leftwards_handler abort_handler) + + (\result_of_folding_over_321. f2 4 result_of_folding_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) + (\result_of_folding_over_4321. f2 5 result_of_folding_over_4321 continue_leftwards_handler abort_handler) abort_handler - ; and so on + ; 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, where the head `5` is supplied to `f2`; 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 +rightmost stage, where the head `4` is supplied to `f2`, 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 +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 - (\result_of_fold_over_4321. f 5 result_of_fold_over_4321 ) + (\result_of_folding_over_4321. f2 5 result_of_folding_over_4321 ) -*to* the application of `f` to `4` as its "continue" handler. The application of `f` +*to* the application of `f2` to `4` as its "continue" handler. The application of `f2` to `4` can decide whether this handler, or the other, "abort" handler, should be given an argument and constitute its result. @@ -491,33 +520,32 @@ 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. +evaluated anyway. We don't yet know any way to prevent that. Later, we'll see +ways to *guarantee* one evaluation order rather than another. Of +course, in any real computing environment you'll know in advance that 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 empty = \f2 z continue_handler abort_handler. continue_handler z in + let make_list = \h t. \f2 z continue_handler abort_handler. + t f2 z (\sofar. f2 h sofar continue_handler abort_handler) abort_handler in let isempty = \lst larger_computation. lst - ; here's our f + ; here's our f2 (\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 + ; here's the continue_handler for the leftmost application of f2 larger_computation ; here's the abort_handler larger_computation in let extract_head = \lst larger_computation. lst - ; here's our f + ; here's our f2 (\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 + ; here's the continue_handler for the leftmost application of f2 larger_computation ; here's the abort_handler larger_computation in @@ -541,38 +569,56 @@ 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: - - let make_list = \h t. \f z continue_handler abort_handler. - 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 - (\hd sofar continue_handler abort_handler. abort_handler hd) - junk - larger_computation - larger_computation - -3. To extract tails efficiently, too, it'd be nice to fuse the apparatus developed - in these v5 lists with the ideas from [v4](/advanced/#index1h1) lists. - But that also is left as an exercise. +> *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 `f2` 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**. What's that? +> +> Well, the right fold of `f` over a list `[a;b;c;d;e]`, using starting value z, is: +> +> f a (f b (f c (f d (f e z)))) +> +> 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: +> +> f (f (f (f (f z a) b) c) d) e +> +> or, if we preferred the arguments to each `f` flipped: +> +> f e (f d (f c (f b (f a z)))) +> +> Recall we implemented v3 lists as their own right-fold functions. We could +> instead implement lists as their own left-fold functions. To do that with our +> v5 lists, we'd replace above: +> +> let make_list = \h t. \f2 z continue_handler abort_handler. +> f2 h z (\z. t f2 z continue_handler abort_handler) abort_handler +> +> Having done that, now `extract_head` can return the leftmost head +> directly, using its `abort_handler`: +> +> let extract_head = \lst larger_computation. lst +> (\hd sofar continue_handler abort_handler. abort_handler hd) +> junk +> larger_computation +> larger_computation +> +> 3. To extract tails efficiently, too, it'd be nice to fuse the apparatus +> developed in these v5 lists with the ideas from +> [v4](/advanced_lambda/#index1h1) lists. But that is left as an exercise. +