CSE413 Notes for Monday, 1/29/24

I continued our discussion of scope as a way to better understand how the OCaml environment works. As I have mentioned before, the right way to think of an OCaml program is as a series of bindings. We have seen that let definitions introduce bindings into the top level environment, as in:

        # let x = 3;;
        val x : int = 3
        # let y = 17;;
        val y : int = 17
        # let f(n) = 2 * n;;
        val f : int -> int = <fun>
These three definitions introduce three bindings in the top-level environment for variables x and y and a function called f. We have also seen let expressions and it is important to understand that they do not introduce bindings into the top-level environment, as in:

        # let z = 19 in x + y + z;;
        - : int = 39
        # z;;
        Error: Unbound value z
A let expression entered at the top level introduces its own scope that is inside of the global scope:

        +-------------------------------------+
        |   let x = 3                         |
        |   let y = 17                        |
        |   let f(n) = 2 * n                  |
        | +---------------------------------+ |
        | | let z = 19 in x + y + z         | |
        | +---------------------------------+ |
        +-------------------------------------+
I revisited an example from the previous lecture where we considered what value the following code would produce:

        let y = 2
        let f(n) =
            let x =
                let n = 3
                in 10 * (n + y)
            and y = 100 * n
            in x + y + n
        
        f(10)
The call on f(10) evaluates to 1060, as we saw in the previous lecture. I showed this picture of the various scopes being introduced:

        +-------------------------------------+
        |   let y = 2                         |
        | +---------------------------------+ |
        | | let f(n) =                      | |
        | |   +---------------------------+ | |
        | |   | let x =                   | | |
        | |   |       +-----------------+ | | |
        | |   |       | let n = 3       | | | |
        | |   |       | in 10 * (n + y) | | | |
        | |   |       +-----------------+ | | |
        | |   | and y = 100 * n           | | |
        | |   | in x + y + n              | | |
        | |   +---------------------------+ | |
        | +---------------------------------+ |
        |                                     |
        |   f(10)                             |
        +-------------------------------------+
This code introduces two bindings at the top level: one for a variable y with the value 2 and one for a function called f. All of the other bindings introduced here are in inner scopes. I mentioned that I really should somehow indicate that the parameter n for function f is inside its scope along with the let expression that defines local variables x and y.

Remember that the rules of scope are that you use local definitions when they exist and otherwise you repeatedly look to the containing scope and if you don't find the identifier there you look to its containing scope and so on.

The interpretation of y in computing 10 * (n + y) is unusual in that normally we would look to the containing scope which has a binding of y to 100 * n. But order matters and that definition of y comes after this expression, so we instead look outward to find the global binding of y to 2. This is sometimes referred to as a hole in scope. Java has the same issue, as in this example:

        public class Test {
           public static int x = 3;
                
            public static void main(String[] args) {
                System.out.println("x = " + x);
                int x  = 17;
                System.out.println("x = " + x);
            }
        }
This program produces the following output:

        x = 3
        x = 17
Notice that within the same scope the identifier x is interpreted in two different ways because the first println appears before the local definition of x I mentioned on the first lecture that an OCaml program is thought of as a series of bindings introduced by let definitions. One way of understanding the difference between a let definition and a let expression is to think of each let definition as introducing a nested let expression:

         let x = 3 in
             let y = 17 in
                let f(n) = 2 * n in
                    ...
We know by the rules of static scope that the most local definition takes precedence, so when we introduce a new binding for a variable or function, that new binding is used in place of the old binding.

Then I spent a few minutes discussing arrays in OCaml. Arrays are like lists with two important differences: they have a fixed size and the elements are mutable. You can define an array by surrounding a sequence of values with vertical bar characters inside of brackets:

        # let a = [| 38; 42; 75 |];;
        val a : int array = [|38; 42; 75|]
There is a length function for an array just as with lists, but it runs in O(1) time instead of O(n) time:

        # Array.length(a);;
        - : int = 3
And there are versions of familiar functions like map that can be used to manipulate an array. The function Array.map returns a new array without changing the original array:

        # let a2 = Array.map (fun x -> 2 * x) a;;
        val a2 : int array = [|76; 84; 150|]
        # a;;
        - : int array = [|38; 42; 75|]
You can refer to individual elements by using the notation .(i) where i is an index. You can change an array's value by using the <- operator, as in:

        # a.(0) <- 100;;
        - : unit = ()
        # a;;  
        - : int array = [|100; 42; 75|]
In functional programming we generally try to avoid what are called side effects. In an earlier lecture I asked when you would know that given a function f that returns an int that you can replace:

        f(n) + f(n)
with:
        2 * f(n)
The answer is that you can make this substition if the function f has no side effects. This property is known as referential transparency.

Notice that the mutating expression indicated a return type of "unit = ()". The unit type is used in OCaml for expressions that don't evaluate to anything. These expressions are only useful if they produce some kind of side effect like mutating a value or printing something. Here is another example:

        # print_string("Hello world!\n");;
        Hello world!
        - : unit = ()
This function had the side effect of printing a line of output, but it didn't return anything. OCaml has a special rule for expressions of type unit that you can chain them together separated by semicolons, as in:

        # print_string("hello\n"); print_endline("there");;
        hello
        there
        - : unit = ()
The print_endline function automatically includes an end-of-line after the string it is printing. You can also have an actual computation as the last element in such a sequence:

        # print_endline("hello"); 2 * 3 + 4;;
        hello
        - : int = 10
Notice that here the expression evaluates to the value 10 of type int.

I reminded people of the following code we looked at in the previous lecture:

        let x = 3
        let f(n) = x * n
        f(8)
        let x = 5
        f(8)
The definition of function f refers to n, which is defined in the function itself (the parameter passed to it), but f also refers to a variable that is not defined in the function itself, the variable x. Variables that are not defined within a function's scope are referred to as free variables.

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 OCaml manage to figure that out?

The answer involves understanding two important concepts:

So in OCaml we really should think of function definitions as being a pair of things: some code to be evaluated when the function is called and an environment to use in executing that code. This pair has a name. We refer to this as the closure of a function.

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 let definition that fully evaluates the code included in the definition versus a function definition that delays evaluating the code used in the definition. For example, I included some expressions that included calls on printing functions to show that let definitions are fully evaluated.

        # let x = 3;;
        val x : int = 3
        # let y = print_endline("hello"); 2 * x;;
        hello
        val y : int = 6
        # let f1(n) = print_endline("hello"); 2 * x + n;;
        val f1 : int -> int = <fun>
        # f1(3);;
        hello
        - : int = 9
        # f1(10);;
        hello
        - : int = 16
For a let definition, the print is performed when you type in the definition (indicating that OCaml is evaluating the code at that time). For the function definition, the print happens only when the function is called, indicating that OCaml delayed evaluating the expression until individual calls are made.

I gave one more example of a function definition to really understand the implications of what it means to have a closure.

        # let a = [|17; 45; 9|];;
        val a : int array = [|17; 45; 9|]
        # let b = 100;;
        val b : int = 100
        # let f(c) = print_endline("hello"); a.(0) + b + c;;
        val f : int -> int = <fun>
        # f(8);;
        hello
        - : int = 125
We begin by defining an array called a whose 0 element has the value 17. We then define a variable b with the value 100. And then we define a function f that prints a line of output with "hello" and then returns the sum of the zero-element of a, b, and c. In this case, the free variables are a and b. The parameter c is defined within the function's scope. When we call it passing 8 to c, we get the result 125 (17 + 100 + 8).

We know that changing b will not change the behavior of the function because it keeps track of the environment that existed at the point that we defined it. But what about the array a? Because the array is mutable, we can change that value and that changes the behavior of the function:

        # b = 0;;
        - : bool = false
        # let b = 0;;
        val b : int = 0
        # a.(0) <- 10;;
        - : unit = ()
        # f(8);;
        hello
        - : int = 118
As expected, resetting b to 0 changed nothing. But resetting the zero-index element of a to 10 changed the computation. Now the function prints the line of output and returns 118 (10 + 100 + 8). This points out an important property of mutable state, that it can lead functions to change their behavior.

I then spent some time discussing why it is difficult in a language like Java to create a closure. I first showed some code that used inheritance to define a variation of the Stack class that overrides the push method to call super.push twice:

        import java.util.*;
        
        class MyStack extends Stack<Integer> {
            public Integer push(Integer n) {
                super.push(n);
                return super.push(n);
            }
        }
        
        public class StackStuff {
            public static void main(String[] args) {
                Stack<Integer> s = new Stack<>();
                s.push(8);
                s.push(10);
                s.push(17);
                s.push(24);
                while (!s.isEmpty()) {
                    System.out.println(s.pop());
                }
            }
        }
When we compiled and ran it, we could see that every value was being duplicated in the stack:

        24
        24
        17
        17
        10
        10
        8
        8
I mentioned that in Java you can define an anonymous inner class that uses inheritance the same way that the MyStack class does:

        import java.util.*;
        
        public class StackStuff2 {
            public static void main(String[] args) {
                Stack<Integer> s = new Stack<>() {
                    public Integer push(Integer n) {
                        super.push(n);
                        return super.push(n);
                    }
                };
                s.push(8);
                s.push(10);
                s.push(17);
                s.push(24);
                while (!s.isEmpty()) {
                    System.out.println(s.pop());
                }
            }
        }
This class had the same behavior. But what if we introduce a local variable and try to access it inside the inner class? That would introduce a free variable.

        public static void main(String[] args) {
	    int x = 3;
            Stack<Integer> s = new Stack<>() {
                public Integer push(Integer n) {
		    super.push(x);
                    super.push(n);
                    return super.push(n);
                }
            };
            ...
This is very tricky because x is a stack-allocated variable. How can the inner class preserve access to it? It would be even worse if this code appeared in a method called by main that returned a reference to the stack that it has constructed. That method's local variables would be freed up, so how could the stack keep a reference to a variable that no longer exists?

But this compiled and ran and behaved as expected by pushing a 3 on top of the stack along with two copies of each value. That would seem to be a closure. This inner class seems to be preserving the environment in which it was defined, including that free variable x that was a local variable in main. But looks can be deceiving. I asked if anyone could think of a way to test whether it really is keeping track of that local variable and someone suggested that we could change x later in the program:

        public static void main(String[] args) {
            int x = 3;
            Stack<Integer> s = new Stack<>() {
                public Integer push(Integer n) {
                    super.push(x);
                    super.push(n);
                    return super.push(n);
                }
            };
            s.push(8);
	    x = 10;
            s.push(10);
            ...
This program did not compile. The compiler said:

        StackStuff2.java:8: error: local variables referenced from an inner
        class must be final or effectively final
In other words, Java is cheating. It treated our local variable x as if it were a constant. That means it wasn't a free variable at all. Java does not have true closures. C#, on the other hand, has true closures and there is a lot of work that goes into making that work.

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.


Stuart Reges
Last modified: Wed Feb 14 17:18:40 PST 2024