val x = 3; fun f(n) = x * n; f(8); val x = 5; f(8);We found that the function uses the binding of x to 3 that exists when the function is defined. Changing the binding for x does not change the behavior of the function. My question is, how does that work? How does ML manage to figure that out?
The answer involves understanding two important concepts:
Remember that functions can have free variables in them, as in our function f that refers to a variable x that is not defined inside the function. The idea of a closure is that we attach a context to the code in the body of the function to "close" all of these stray references.
We explored some examples to understand the difference between a val declaration that fully evaluates the code included in the declaration versus a function definition that delays evaluating the code used in the definition. For example, I included some sequence expressions with calls on print to show that val declarations are fully evaluated.
- val x = 3; val x = 3 : int - val y = (print("hello\n"); 2 * x); hello val y = 6 : int - fun f1(n) = (print("hello\n"); 2 * x); val f1 = fn : 'a -> int - f1(3); hello val it = 6 : int - f1(10); hello val it = 6 : intFor a val definition, the print is performed when you type in the definition (indicating that ML is evaluating the code at that time). For the function definition, the print happens only when the function is called, indicating that ML delayed evaluating the expression until individual calls are made.
Then I started a new topic: lexical scope. This is just one example of a number of related topics that have to do with the static properties of a program versus the dynamic properties of a program. The terms compile time and run time are related terms because we can think of these as the static properties that can be deduced ahead of time by a program like a compiler versus the dynamic properties that are apparent only when the program actually executes.
Lexical scope is a static property, which is why it is sometimes referred to as static scoping (e.g., in the wikipedia entry about scope). I started a two-column list of static versus dynamic properties:
static dynamic before execution during execution compile-time run-time compilers interpreters lexical scope dynamic scope compile-time binding polymorphism (run-time binding, late binding dynamic dispatch) control flowLexical scope will be familiar because Java uses it. Consider, for example, the following program:
public class Scope { public static int x = 10; public static void main(String[] args) { System.out.println(x); if (x > 0) { int x = 20; System.out.println(x); } int x = 30; System.out.println(x); } }In Java, every set of curly braces introduces a new lexical scope (a new region of the program known as a block). In the program above, there is an outer scope for the overall class and an inner scope for method main and for the if:
+------------------------------------------------+ | public class Scope { | | public static int x = 10; | | +------------------------------------------+ | | | public static void main(String[] args) { | | | | System.out.println(x); | | | | +----------------------------+ | | | | | if (x > 0) { | | | | | | int x = 20; | | | | | | System.out.println(x); | | | | | | } | | | | | +----------------------------+ | | | | int x = 30; | | | | System.out.println(x); | | | | } | | | +------------------------------------------+ | | } | +------------------------------------------------+We want to pay attention to the identifier "x" as it is used in each of these scopes. There is a global variable x declared in the outer scope that is printed by the first call on println. There is a local x that shadows the global x that is defined inside the if block and is printed by the second println. And there is a different local x that shadows the global x and that is printed by the third println. So the output of this program is 10, 20, 30.
I then asked people to think about how scope works in ML. ML uses lexical scoping, but where do the scopes come from? Function declarations introduce a scope for the parameters that are listed for the function, as in:
fun f(m, n) = [this creates a local scope for identifiers m and n]; (* m and n are not visible here after the function definition *) fn x => [this creates a local scope for identifier x]We also get a different local scope for each let construct. You can think of each "let" as introducing its own scope box. Because there can be let constructs inside of let constructs, we can have scopes inside of scopes.
We discussed this example with nested scopes introduced by let constructs:
val y = 2; fun f(n) = let val x = let val n = 3 in 10 * (n + y) end val y = 100 * n in x + y + n end;Below is a diagram of the scopes that are introduced by the funciton defintion and the let constructs:
+-------------------------------------+ | val y = 2; | | +---------------------------------+ | | | fun f(n) = | | | | +---------------------------+ | | | | | let val x = | | | | | | +-----------------+ | | | | | | | let val n = 3 | | | | | | | | in 10 * (n + y) | | | | | | | | end | | | | | | | +-----------------+ | | | | | | val y = 100 * n | | | | | | in x + y + n | | | | | | end; | | | | | +---------------------------+ | | | +---------------------------------+ | | | | f(6); | | ... | +-------------------------------------+This diagram includes an outer scope for definitions in the top-level environment.
Inside function f, when we go to compute the expression 10 * (n + y), we have to figure out which n and y to use. We find a local definition for n in that innermost scope, so we use that. There is a definition for y in the containing scope, but that definition comes after this one and order matters. So in this case, the y that is found is the y from the global environment and we compute the answer as 10 * (3 + 2) or 50.
We then compute the value of y to be 100 * n. In this case, we don't find a value n in the scope of the let, but we find it in the containing scope for the function definition (the parameter n). We are simulating the fall f(6), so for that particular call this expression will evaluate to 600.
The final expression we have to evaluate is x + y + n. We find local definitions for x and y (50 and 600) and n refers to the value passed as a parameter. So the overall expression evaluates to 50 + 600 + 6 or 656.
I then briefly discussed the idea of dynamic scope. Dynamic scope is less common in programming languages today. If you're writing an interpreter, it's easier to do dynamic scoping, so it was common in the early days for interpretted languages to use it. But most people found it sufficiently confusing that it is rarely used in modern languages.
In dynamic scope, you pay attention to the order in which definitions are encountered. When you find a reference to an identifier, you search backwards to find the most recent definition of that identifier. As an example, I asked people to consider the following Java program:
public class Scope2 { private static int x = 3; public static void one() { x *= 2; System.out.println(x); } public static void two() { int x = 5; one(); System.out.println(x); } public static void main(String[] args) { one(); two(); int x = 2; one(); System.out.println(x); } }We know that by Java's lexical scoping rules, the identifier x in method one always refers to the global field x. But that wouldn't always be true under dynamic scope rules. For example, when method two is called, it defines a local variable x and then calls method one. Under dynamic scope, that local variable in method two would be the x that method one uses on that particular call because it will be the most recently encountered definition of x.
I mentioned that I'm not expecting everyone to master the dynamic scope approach. But there are some languages that still use it. For example, the following is a shell script that is basically a translation of the previous Java program:
#!/bin/sh x=3 one() { x=`expr 2 \* $x` echo $x } two() { local x=5 one echo $x } main() { one two local x=2 one echo $x } mainShell scripts use dynamic scope. The output of this shell script is:
6 10 10 4 4The function one prints three different x values. Below is a diagram of the dynamic scope view of this script:
invoke shell script +-------------------------+ | global x 3 | | | | function main | | +---------------------+ | | | function one | | | | +-------------+ | | | | | refer to x | | | | | +-------------+ | | | | | | | | function two | | | | +-----------------+ | | | | | local x 5 | | | | | | | | | | | | function one | | | | | | +-------------+ | | | | | | | refer to x | | | | | | | +-------------+ | | | | | | refer to x | | | | | +-----------------+ | | | | local x 2 | | | | | | | | function one | | | | +-------------+ | | | | | refer to x | | | | | +-------------+ | | | | refer to x | | | +---------------------+ | +-------------------------+I then spent some time discussing why it is difficult in a language like Java to create a closure. We looked again at the main method from our scope example and I asked people to consider what would be involved in defining a closure at three different points of execution:
public static void main(String[] args) { System.out.println(x); // form a closure here if (x > 0) { int x = 20; System.out.println(x); // form a closure here } int x = 30; System.out.println(x); // form a closure here }In each case we would have to preserve the environment that existed at that point in program execution. It's important to realize that if we were to form a closure to define, say, a function, then the function we define might not be called until much later in time. The biggest problem here comes from the local variables. In Java we expect local variables to come into existence and then to go away when we exit a scope. To form a closure, we'd have to somehow preserve those local variables for a longer period of time.
I quickly pointed out that in Java, you can have an inner class that makes reference to fields and methods from the other class, as in:
public class ArrayIntList extends AbstractIntList { <fields and methods of ArrayIntList> private class ArrayIterator implements Iterator<Integer> { private int position; // current position within the list private boolean removeOK; // whether it's okay to remove now // post: constructs an iterator for the given list public ArrayIterator() { position = 0; removeOK = false; } public boolean hasNext() { return position < size(); } public Integer next() { if (!hasNext()) throw new NoSuchElementException(); int result = get(position); position++; removeOK = true; return result; } public void remove() { if (!removeOK) throw new IllegalStateException(); ArrayIntList.this.remove(position - 1); position--; removeOK = false; } } }This a common pattern in Java to implement iterators as inner classes. One of the key lines of code to look at is the body of the hasNext method. The iterator has a field called position that it compares to the value of "size()". That's a method call, but on what method? It's certainly not a call on "this.size()" because the iterator does not have a method called size. Instead Java considers this a call on the size method of the outer class.
But how is this done? How can an object of an inner class call a method of the outer class? The answer is that Java provides something like a function closure. The inner class exists within an enviroment of an outer class with fields and methods. The inner object is constructed so that it has access to this outer environment. In other words, the inner object has a closure relative to this outer environment.
You can see this even more clearly in the remove method that has a call on the outer objects remove method. The call is on "ArrayIntlist.this.remove" which is a notation that specifically says, "call this on the outer object's remove method."
So Java provides something like a function closure when you have one class inside another. The inner class exists within an environment of an outer class with fields and methods. The inner object is constructed so that it has access to this outer environment. In other words, the inner object has a closure that includes this outer enviroment.
This can get even more tricky when you use what are known as anonymous inner classes that can appear in the middle of a method body. For example, we looked at this simple code that pushes 4 values on top of a stack and then prints them as they are popped off:
import java.util.*; public class StackStuff { public static void main(String[] args) { Stack<Integer> s = new Stack<Integer>(); s.push(8); s.push(10); s.push(17); s.push(24); while (!s.isEmpty()) { System.out.println(s.pop()); } } }I said that Java allows you to define an anonymous class in the middle of this code. The anonymous class that I defined overrides the push method to push each value twice. I did this by replacing this line of code:
Stack<Integer> s = new Stack<Integer>();with this code:
Stack<Integer> s = new Stack<Integer>() { public Integer push(Integer n) { super.push(n); return super.push(n); } };It's rather unusual that you can define classes in this way. I asked people whether they thought that this inner class could access local variables of the method, as in:
int x = 12; Stack<Integer> s = new Stack<Integer>() { public Integer push(Integer n) { super.push(x); super.push(n); return super.push(n); } };We got a very interesting error message when we tried to compile this. Java said that we couldn't reference a local variable inside the inner class. So Java is not providing us a true closure. It said that the variable would have to be declared to be final, meaning that it has to be a constant:
final int x = 12; Stack<Integer> s = new Stack<Integer>() { public Integer push(Integer n) { super.push(x); super.push(n); return super.push(n); } };This is yet another example of mutable state causing difficulty that immutability resolves. It's difficult to provide access to a local variable of a method, but it's not difficult to provide access to an immutable constant.