index ddee5f7..719057d 100644 (file)
@@ -1,8 +1,8 @@
-# Assignment 7
-
There is no separate assignment 6. (There was a single big assignment for weeks 5 and 6, and
we're going to keep the assignment numbers in synch with the weeks.)

There is no separate assignment 6. (There was a single big assignment for weeks 5 and 6, and
we're going to keep the assignment numbers in synch with the weeks.)

+[[!toc]]
+
## Evaluation order in Combinatory Logic

1. Give a term that the lazy evaluators (either [[the Haskell
## Evaluation order in Combinatory Logic

1. Give a term that the lazy evaluators (either [[the Haskell
@@ -45,7 +45,7 @@ add the last pieces to make the program function.
You can find the skeleton code [[here|/code/untyped_evaluator.ml]].

The first place you need to add code is in the `free_in` function. You already
You can find the skeleton code [[here|/code/untyped_evaluator.ml]].

The first place you need to add code is in the `free_in` function. You already
-wrote such a function [[in a previous homework|assignment5#occurs_free]], so this
+wrote such a function [[in a previous homework|exercises/assignment5-6#occurs_free]], so this
part should be easy. The intended behavior is for the function to return results
like this:

part should be easy. The intended behavior is for the function to return results
like this:

@@ -83,138 +83,20 @@ This part of the exercise is the "VB" part of that code.
You'll see that in the `eval` function, a new kind of value `Closure (ident) (term) (env)`
is used. What's that about?

You'll see that in the `eval` function, a new kind of value `Closure (ident) (term) (env)`
is used. What's that about?

-The `Closure`s are for handling the binding of terms that have locally free variables in them. Let's
-see how this works. For exposition, I'll pretend that the code you're working with has
-primitive numbers in it, though it doesn't. (But the fuller code it's simplified from does; see below.)
-Now consider:
-
-    term                             environment
-    ----                             -----------
-    (\w.(\y.y) w) 2                  []
-    (\y.y) w                         [w->2]
-    y                                [y->w, w->2]
-
-In the first step, we bind `w` to the term `2`, by saving this association in our environment. In the second step, we bind `y` to the term `w`. In the third step, we would like to replace `y` with whatever its current value is according to our scorecard/environment. But a naive handling of this would replace `y` with `w`; and that's not the right result, because `w` should itself be mapped onto `2`. On the other hand, in:
-
-    term                             environment
-    ----                             -----------
-    (\x w. x) w 2                    []
-    (\w. x) 2                        [x->w]
-    x                                [w->2, x->w]
-
-Now our final term _should_ be replaced with `w`, not with `2`. So evidently it's not so simple as just recursively re-looking up variables in the environment.
-
-A good strategy for handling these cases would be not to bind `y` to the term `w`, but rather to bind it to _what the term `w` then fully evaluates to_. Then in the first case we'd get:
-
-<pre>
-term                             environment
-----                             -----------
-(\w.(\y.y) w) 2                  []
-(\y.y) w                         [w->2]
-y                                [<b>y->2</b>, w->2]
-</pre>
-
-And at the next step, `y` will evaluate directly to `2`, as desired. In the other example, though, `x` gets bound to `w` when `w` is already free. (In fact the code skeleton we gave you won't permit that to happen; it refuses to perform any applications except when the arguments are "result values", and it doesn't count free variables as such. As a result, other variables can never get bound to free variables.)
-
-So far, so good.
-
-But now consider the term:
-
-    (\f. x) ((\x y. y x) 0)
-
-Since the outermost head `(\f. x)` is already a `Lambda`, we begin by evaluating the argument `((\x y. y x) 0)`. This results in:
-
-    term                             environment
-    ----                             -----------
-    (\f. x) (\y. y x)                [x->0]
-
-But that's not right, since it will result in the variable `x` in the head also being associated with the argument `0`. Instead, we want the binding of `x` to `0` to be local to the argument term `(\y. y x)`. For the moment, let's notate that like this:
-
-<pre>
-term                             environment
-----                             -----------
-(\f. x)<sub>1</sub> (\y. y x)<sub>2</sub>              []<sub>1</sub>   [x->0]<sub>2</sub>
-</pre>
-
-Now, let's suppose the head is more complex, like so:
-
-    (\f. (\x. f x) I) ((\x y. y x) 0)
-
-That might be easier to understand if you transform it to:
-
-    let f = (let x = 0 in \y. y x) in
-    let x = I in
-    f x
-
-Since the outermost head `(\f. (\x. f x) I)` is already a `Lambda`, we begin by evaluating the argument `((\x y. y x) 0)`. This results in:
-
-<pre>
-term                             environment
-----                             -----------
-(\f. (\x. f x) I)<sub>1</sub> (\y. y x)<sub>2</sub>    []<sub>1</sub> [x->0]<sub>2</sub>
-</pre>
-
-Now the argument is not itself any longer an `App`, and so we are ready to evaluate the outermost application, binding `f` to the argument term. So we get:
-
-<pre>
-term                             environment
-----                             -----------
-((\x. f x) I)<sub>1</sub>                   [f->(\y. y x)<sub>2</sub>]<sub>1</sub> [x->0]<sub>2</sub>
-</pre>
-
-Next we bind `x` to the argument `I`, getting:
-
-<pre>
-term                             environment
-----                             -----------
-(f x)<sub>1</sub>                           [x->I, f->(\y. y x)<sub>2</sub>]<sub>1</sub> [x->0]<sub>2</sub>
-</pre>
-
-Now we have to apply the value that `f` is bound to to the value that `x` is bound to. But notice there is a free variable `x` in the term that `f` is bound to. How should we interpret that term? Should the evaluation proceed:
-
-<pre>
-(\y. y <b>x<sub>1</sub></b>) x<sub>1</sub>                    [x->I, ...]<sub>1</sub> [x->0]<sub>2</sub>
-y x<sub>1</sub>                             [y->I, x->I, ...]<sub>1</sub> [x->0]<sub>2</sub>
-I I
-I
-</pre>
-
-using the value that `x` is bound to in context<sub>1</sub>, _where the `f` value is applied_? Or should it proceed:
-
-<pre>
-(\y. y <b>x<sub>2</sub></b>) x<sub>1</sub>                    [x->I, ...]<sub>1</sub> [x->0]<sub>2</sub>
-y x<sub>2</sub>                             [y->I, x->I, ...]<sub>1</sub> [x->0]<sub>2</sub>
-I 0
-0
-</pre>
-
-using the value that `x` was bound to in context<sub>2</sub>, _where `f` was bound_?
-
-In fact, when we specified rules for the Lambda Calculus, we committed ourselves to taking the second of these strategies. But both of the kinds of binding described here are perfectly coherent. The first is called "dynamic binding" or "dynamic scoping" and the second is called "lexical or static binding/scoping". Neither is intrinsically more correct or appropriate than the other. The first is somewhat easier for the people who write implementations of programming languages to handle; so historically it used to predominate. But the second is easier for programmers to reason about. In Scheme, variables are bound in the lexical/static way by default, just as in the Lambda Calculus; but there is special vocabulary for dealing with dynamic binding too, which is useful in some situations. (As far as I'm aware, Haskell and OCaml only provide the lexical/static binding.)
-
-In any case, if we're going to have the same semantics as the untyped Lambda Calculus, we're going to have to make sure that when we bind the variable `f` to the value `\y. y x`, that (locally) free variable `x` remains associated with the value `2` that `x` was bound to in the context where `f` is bound, not the possibly different value that `x` may be bound to later, when `f` is applied. One thing we might consider doing is _evaluating the body_ of the abstract that we want to bind `f` to, using the then-current environment to evaluate the variables that the abstract doesn't itself bind. But that's not a good idea. What if the body of that abstract never terminates? The whole program might be OK, because it might never go on to apply `f`. But we'll be stuck trying to evaluate `f`'s body anyway, and will never get to the rest of the program. Another thing we could consider doing is to substitute the `2` in for the variable `x`, and then bind `f` to `\y. y 2`. That would work, but the whole point of this evaluation strategy is to avoid doing those complicated (and inefficient) substitutions. Can you think of a third idea?
-
-What we will do is have our environment associate `f` not just with a `Lambda` term, but also with the environment that was in place _when_ `f` was bound. Then later, if we ever do use `f`, we have that saved environment around to look up any free variables in. More specifically, we'll associate `f` not with the _term_:
-
-    Lambda("y", BODY)
-
-but instead with the `Closure` structure:
-
-    Closure("y", BODY, SAVED_ENV)
+[[Read about Closures here.|/topics/week7_environments_and_closures]]

-where `BODY` is the term that constituted the body of the corresponding `Lambda`, and `SAVED_ENV` is the environment in place when `f` is being bound. (In this simple implementation, we will just save the _whole environment_ then in place. But in a more efficient implementation, we'd sift through it and only keep those bindings for variables that are free in `BODY`. That would take up less space.)

-So that's what's going on with those `Closure`s. In the simple code we gave you to work with, we just made these another clause in the `term` datatype. That's really not correct. `Closure`s aren't terms. The syntax for our language doesn't have any constituents that get parsed into `Closure`s. `Closure`s are only created *during the course of evaluating terms*: specifically, when a variable gets bound to an abstract, which may itself contain variables that are locally free (not bound by the abstract itself). So really we should have two datatypes, one for terms, and another for the *results* (sometimes called "values") that terms can evaluate to. `Closure`s are results, but they aren't terms. `App`s are terms, but not results. If we had primitive numbers or other constants in our language, they might be both terms and results. In the fuller code from which your homework is simplified, this is how the types are in fact defined. But it makes things more complicated. So to keep things simple for the homework, we just pretended like `Closure`s were a new, exotic kind of `term`.
+So that's what's going on with those `Closure`s. In the simple code we gave you to work with, we just made these another clause in the `term` datatype. That's really not correct. `Closure`s aren't terms. The syntax for our language doesn't have any constituents that get parsed into `Closure`s. `Closure`s are only created *during the course of evaluating terms*: specifically, when a variable gets bound to an abstract, which may itself contain variables that are locally free (not bound by the abstract itself). So really we should have two datatypes, one for terms, and another for the *results* (sometimes called "values") that terms can evaluate to. `Closure`s are results, but they aren't terms. (In later weeks, we will see more examples of results that aren't terms, but can only be generated during the course of a computation.) `App`s are terms, but not results. If we had primitive numbers or other constants in our language, they might be both terms and results. In the fuller code from which your homework is simplified, this is how the types are in fact defined. But it makes things more complicated. So to keep things simple for the homework, we just pretended like `Closure`s were a new, exotic kind of `term`.

In any case, now you know what's going on with the `Closure`s, and you should be able to complete the missing pieces of the `eval` function in the code skeleton linked above.

In any case, now you know what's going on with the `Closure`s, and you should be able to complete the missing pieces of the `eval` function in the code skeleton linked above.

-If you've completed all the (eight) missing parts correctly, then you should be able to compile the code skeleton, and use it as described in the comments at the start of the code.
+If you've completed all the missing parts correctly (there are six gaps for the previous stage of the homework, and two gaps for this stage), then you should be able to compile the code skeleton, and use it as described in the comments at the start of the code.

## Fuller interpreter

## Fuller interpreter

-We've also prepared a fuller version of the interpreter, that has user-friendly input
-and printing of results. We'll provide a link to that shortly. It
+We've also prepared [[a fuller version of the interpreter, that has user-friendly input
+and printing of results|/topics/week7_untyped_evaluator]]. It
will be easiest for you to understand that code if you've
completed the gaps in the simplified skeleton linked above.

will be easiest for you to understand that code if you've
completed the gaps in the simplified skeleton linked above.