X-Git-Url: http://lambda.jimpryor.net/git/gitweb.cgi?p=lambda.git;a=blobdiff_plain;f=week4.mdwn;h=f92ea169d4e09cc67c097cb104be303328dd607f;hp=6613fb407220e8beac7532e117cc902bb5834a81;hb=0ba095c6cd0331481a1454283ceeb14be690b5b8;hpb=9b65b6793ba00e0b9f42a5851f0b5185623abae8 diff --git a/week4.mdwn b/week4.mdwn index 6613fb40..f92ea169 100644 --- a/week4.mdwn +++ b/week4.mdwn @@ -6,75 +6,82 @@ A: That's easy: let `T` be an arbitrary term in the lambda calculus. If `T` has a fixed point, then there exists some `X` such that `X <~~> TX` (that's what it means to *have* a fixed point). -
```-let W = \x.T(xx) in
-let X = WW in
-X = WW = (\x.T(xx))W = T(WW) = TX
-```
+
``````let L = \x. T (x x) in
+let X = L L in
+X ≡ L L ≡ (\x. T (x x)) L ~~> T (L L) ≡ T X
+``````
Please slow down and make sure that you understand what justified each of the equalities in the last line. -#Q: How do you know that for any term `T`, `YT` is a fixed point of `T`?# +#Q: How do you know that for any term `T`, `Y T` is a fixed point of `T`?# A: Note that in the proof given in the previous answer, we chose `T` -and then set `X = WW = (\x.T(xx))(\x.T(xx))`. If we abstract over -`T`, we get the Y combinator, `\T.(\x.T(xx))(\x.T(xx))`. No matter -what argument `T` we feed Y, it returns some `X` that is a fixed point +and then set `X ≡ L L ≡ (\x. T (x x)) (\x. T (x x))`. If we abstract over +`T`, we get the Y combinator, `\T. (\x. T (x x)) (\x. T (x x))`. No matter +what argument `T` we feed `Y`, it returns some `X` that is a fixed point of `T`, by the reasoning in the previous answer. #Q: So if every term has a fixed point, even `Y` has fixed point.# A: Right: - let Y = \T.(\x.T(xx))(\x.T(xx)) in - Y Y = \T.(\x.T(xx))(\x.T(xx)) Y - = (\x.Y(xx))(\x.Y(xx)) - = Y((\x.Y(xx))(\x.Y(xx))) - = Y(Y((\x.Y(xx))(\x.Y(xx)))) - = Y(Y(Y(...(Y(YY))...))) +
``````let Y = \T. (\x. T (x x)) (\x. T (x x)) in
+Y Y
+≡   \T. (\x. T (x x)) (\x. T (x x)) Y
+~~> (\x. Y (x x)) (\x. Y (x x))
+~~> Y ((\x. Y (x x)) (\x. Y (x x)))
+~~> Y (Y ((\x. Y (x x)) (\x. Y (x x))))
+~~> Y (Y (Y (...(Y (Y Y))...)))
+``````
+ #Q: Ouch! Stop hurting my brain.# -A: Let's come at it from the direction of arithmetic. Recall that we +A: Is that a question? + +Let's come at it from the direction of arithmetic. Recall that we claimed that even `succ`---the function that added one to any number---had a fixed point. How could there be an X such that X = X+1? That would imply that - X = succ X = succ (succ X) = succ (succ (succ (X))) = succ (... (succ X)...) + X <~~> succ X <~~> succ (succ X) <~~> succ (succ (succ X)) <~~> succ (... (succ X)...) In other words, the fixed point of `succ` is a term that is its own successor. Let's just check that `X = succ X`: - let succ = \n s z. s (n s z) in - let X = (\x.succ(xx))(\x.succ(xx)) in - succ X - = succ ((\x.succ(xx))(\x.succ(xx))) - = succ (succ ((\x.succ(xx))(\x.succ(xx)))) - = succ (succ X) +
``````let succ = \n s z. s (n s z) in
+let X = (\x. succ (x x)) (\x. succ (x x)) in
+succ X
+≡   succ ( (\x. succ (x x)) (\x. succ (x x)) )
+~~> succ (succ ( (\x. succ (x x)) (\x. succ (x x)) ))
+≡   succ (succ X)
+``````
+ +You should see the close similarity with `Y Y` here. -You should see the close similarity with YY here. #Q. So `Y` applied to `succ` returns a number that is not finite!# A. Yes! Let's see why it makes sense to think of `Y succ` as a Church numeral: - [same definitions] - succ X - = (\n s z. s (n s z)) X - = \s z. s (X s z) - = succ (\s z. s (X s z)) ; using fixed-point reasoning - = \s z. s ([succ (\s z. s (X s z))] s z) - = \s z. s ([\s z. s ([succ (\s z. s (X s z))] s z)] s z) - = \s z. s (s (succ (\s z. s (X s z)))) +
``````[same definitions]
+succ X
+≡    (\n s z. s (n s z)) X
+~~>  \s z. s (X s z)
+<~~> succ (\s z. s (X s z)) ; using fixed-point reasoning
+≡    (\n s z. s (n s z)) (\s z. s (X s z))
+~~>  \s z. s ((\s z. s (X s z)) s z)
+~~>  \s z. s (s (X s z))
+``````
So `succ X` looks like a numeral: it takes two arguments, `s` and `z`, and returns a sequence of nested applications of `s`... You should be able to prove that `add 2 (Y succ) <~~> Y succ`, -likewise for `mult`, `minus`, `pow`. What happens if we try `minus (Y -succ)(Y succ)`? What would you expect infinity minus infinity to be? +likewise for `mul`, `sub`, `pow`. What happens if we try `sub (Y +succ) (Y succ)`? What would you expect infinity minus infinity to be? (Hint: choose your evaluation strategy so that you add two `s`s to the first number for every `s` that you add to the second number.) @@ -84,13 +91,14 @@ represents arithmetic infinity. It's important to bear in mind the simplest term in question is not infinite: - Y succ = (\f.(\x.f(xx))(\x.f(xx)))(\n s z. s (n s z)) + Y succ = (\f. (\x. f (x x)) (\x. f (x x))) (\n s z. s (n s z)) The way that infinity enters into the picture is that this term has no normal form: no matter how many times we perform beta reduction, there will always be an opportunity for more beta reduction. (Lather, rinse, repeat!) + #Q. That reminds me, what about [[evaluation order]]?# A. For a recursive function that has a well-behaved base case, such as @@ -100,63 +108,63 @@ which we have to make a choice about which beta reduction to perform next: one choice leads to a normal form, the other choice leads to endless reduction: - let prefac = \f n. isZero n 1 (mult n (f (pred n))) in - let fac = Y prefac in - fac 2 - = [(\f.(\x.f(xx))(\x.f(xx))) prefac] 2 - = [(\x.prefac(xx))(\x.prefac(xx))] 2 - = [prefac((\x.prefac(xx))(\x.prefac(xx)))] 2 - = [prefac(prefac((\x.prefac(xx))(\x.prefac(xx))))] 2 - = [(\f n. isZero n 1 (mult n (f (pred n)))) - (prefac((\x.prefac(xx))(\x.prefac(xx))))] 2 - = [\n. isZero n 1 (mult n ([prefac((\x.prefac(xx))(\x.prefac(xx)))] (pred n)))] 2 - = isZero 2 1 (mult 2 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] (pred 2))) - = mult 2 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] 1) - ... - = mult 2 (mult 1 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] 0)) - = mult 2 (mult 1 (isZero 0 1 ([prefac((\x.prefac(xx))(\x.prefac(xx)))] (pred 0)))) - = mult 2 (mult 1 1) - = mult 2 1 - = 2 +
``````let prefact = \f n. iszero n 1 (mul n (f (pred n))) in
+let fact = Y prefact in
+fact 2
+≡   [(\f. (\x. f (x x)) (\x. f (x x))) prefact] 2
+~~> [(\x. prefact (x x)) (\x. prefact (x x))] 2
+~~> [prefact ((\x. prefact (x x)) (\x. prefact (x x)))] 2
+~~> [prefact (prefact ((\x. prefact (x x)) (\x. prefact (x x))))] 2
+≡   [ (\f n. iszero n 1 (mul n (f (pred n)))) (prefact ((\x. prefact (x x)) (\x. prefact (x x))))] 2
+~~> [\n. iszero n 1 (mul n ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] (pred n)))] 2
+~~> iszero 2 1 (mul 2 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] (pred 2)))
+~~> mul 2 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] 1)
+...
+~~> mul 2 (mul 1 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] 0))
+≡   mul 2 (mul 1 (iszero 0 1 (mul 1 ([prefact ((\x. prefact (x x)) (\x. prefact (x x)))] (pred 0)))))
+~~> mul 2 (mul 1 1)
+~~> mul 2 1
+~~> 2
+``````
The crucial step is the third from the last. We have our choice of -either evaluating the test `isZero 0 1 ...`, which evaluates to `1`, +either evaluating the test `iszero 0 1 ...`, which evaluates to `1`, no matter what the ... contains; -or we can evaluate the `Y` pump, `(\x.prefac(xx))(\x.prefac(xx))`, to -produce another copy of `prefac`. If we postpone evaluting the -`isZero` test, we'll pump out copy after copy of `prefac`, and never +or we can evaluate the `Y` pump, `(\x. prefact (x x)) (\x. prefact (x x))`, to +produce another copy of `prefact`. If we postpone evaluting the +`iszero` test, we'll pump out copy after copy of `prefact`, and never realize that we've bottomed out in the recursion. But if we adopt a leftmost/call-by-name/normal-order evaluation strategy, we'll always -start with the `isZero` predicate, and only produce a fresh copy of -`prefac` if we are forced to. +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.# + A. OK: -
```-A(m,n) =
-    | when m == 0 -> n + 1
-    | else when n == 0 -> A(m-1,1)
-    | else -> A(m-1, A(m,n-1))
-
-let A = Y (\A m n. isZero m (succ n) (isZero n (A (pred m) 1) (A (pred m) (A m (pred n))))) in
-```
``extract_fst ≡ \pair. pair (\x y. x)``
-Given a set of types `T`, we define the set of typed lambda terms `Λ_T`, -which is the smallest set such that +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.) -* each type `t` has an infinite set of distinct variables, {x^t}_1, - {x^t}_2, {x^t}_3, ... +> *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. -* If a term `M` has type σ --> τ, and a term `N` has type - σ, then the application `(M N)` has type τ. +The v2 implementation of lists followed a similar strategy: -* If a variable `a` has type σ, and term `M` has type τ, - then the abstract `λ a M` has type `σ --> τ`. + v2list (\h t. do_something_with_h_and_t) result_if_empty -The definitions of types and of typed terms should be highly familiar -to semanticists, except that instead of writing `σ --> τ`, -linguists (following Montague, who followed Church) write `<σ, -τ>`. We will use the arrow notation, since it is more iconic. +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)`. -Some examples (assume that `x` has type `o`): +Now, what we've been imagining ourselves doing with the search through the v3 +list is something like this: - x o - \x.x o --> o - ((\x.x) x) o -Excercise: write down terms that have the following types: + larger_computation (search_through_the_list_for_3) other_arguments - o --> o --> o - (o --> o) --> o --> o - (o --> o --> o) --> o +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. -#Associativity of types versus terms# +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? -As we have seen many times, in the lambda calculus, function -application is left associative, so that `f x y z == (((f x) y) z)`. -Types, *THEREFORE*, are right associative: if `f`, `x`, `y`, and `z` -have types `a`, `b`, `c`, and `d`, respectively, then `f` has type `a ---> b --> c --> d == (a --> (b --> (c --> d)))`. + the_search (\search_result. larger_computation search_result other_arguments) -It is a serious faux pas to associate to the left for types, on a par -with using your salad fork to stir your tea. +What's the advantage of that, you say. Other than to show off how cleverly +you can lift. -#The simply-typed lambda calculus is strongly normalizing# +Well, think about it. Think about the difficulty we were having aborting the +search. Does this switch-around offer us anything useful? -If `M` is a term with type τ in `Λ_T`, then `M` has a -normal form. The proof is not particularly complex, but we will not -present it here; see Berendregt or Hankin. +It could. -Since Ω does not have a normal form, it follows that Ω -cannot have a type in `Λ_T`. We can easily see why: +What if the way we implemented the search procedure looked something like this? - Ω = (\x.xx)(\x.xx) +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. -Assume Ω has type τ, and `\x.xx` has type σ. Then -because `\x.xx` takes an argument of type σ and returns -something of type τ, `\x.xx` must also have type `σ --> -τ`. By repeating this reasoning, `\x.xx` must also have type -`(σ --> τ) --> τ`; and so on. Since variables have -finite types, there is no way to choose a type for the variable `x` -that can satisfy all of the requirements imposed on it. +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 general, there is no way for a function to have a type that can -take itself for an argument. It follows that there is no way to -define the identity function in such a way that it can take itself as -an argument. Instead, there must be many different identity -functions, one for each type. +Why would we do that, you say? Just more flamboyant lifting? -#Typing numerals# +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: -Version 1 type numerals are not a good choice for the simply-typed -lambda calculus. The reason is that each different numberal has a -different type! For instance, if zero has type σ, and `false` -has type `τ --> τ --> τ` for some τ, and one is -represented by the function `\x.x false 0`, then one must have type -`(τ --> τ --> &tau) --> &sigma --> σ`. But this is a -different type than zero! Because numbers have different types, it -becomes impossible to write arithmetic operations that can combine -zero with one. We would need as many different addition operations as -we had pairs of numbers that we wanted to add. -Fortunately, the Church numberals are well behaved with respect to -types. They can all be given the type `(σ --> σ) --> -σ --> σ`. + 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 + +`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](/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) + abort_handler + + ; or, expanding the fold over [4;3;2;1]: + + \f 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) + 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 ) + +*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. + + + +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.