CSE341 Notes for Friday, 4/24/09

We continued our discussion of scope. I began by asking people whether the for loop in Java is syntactic sugar. Most people seemed to think that any for loop could be converted into an equivalent while loop. I asked in general how to translate a loop like this:

for (<init>; <test>; <update>) <statement> We decided to write it as:

<init>; while (<test>) { <statement> <update> } Of course, there are some special cases you have to handle. For example, the test in the for loop can be empty which is translated into a "while (true)" loop. But for the most part the for loop can be translate in this way. But it misses one important aspect of the for loop. The scope of the variables defined in the initialization part are limited to the loop itself. So really we should translate it as:

{ <init>; while (<test>) { <statement> <update> } } Then I spent some time discussing inner classes in Java. We discussed in the previous lecture that 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 Test4 {
            public static void main(String[] args) {
                Stack s = new Stack();
                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 s = new Stack();
with this code:

        Stack s = new Stack() {
            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 s = new Stack() {
            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 s = new Stack() {
            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.

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 turned to a new topic. I said that in the next few lectures we would discuss two important concepts from ML known as structures and signatures. I started with structures.

When you write a large amount of code you find yourself having trouble managing the name space. For example, what if you load two different ML files that each have a function with a particular name? The second file loaded wipes out the definition from the first file. And what if you have a utility like toString that you want to implement for many different types? A structure provides a way to establish independent namespaces. For example, ML has a function called Real.toString that is part of a structure called Real and also a function called Int.toString that is part of a structure called Int.

The basic form of a structure is:

structure <name> = struct <definitions> end The definitions can include the various elements we have been discussing:

I said that we would be looking at an example that I got from Dan Grossman. We will define a structure called Rational that provides a datatype and functions for manipulating rational numbers. We are going to go through several different versions to better understand the details of structures and signatures.

So our overall structure will look like this:

        structure Rational =
        struct
            (* we'll fill in definitions here *)
        end;
We began with a datatype definition. Rational numbers are numbers that can be expressed as a ratio of two integers, so it would make sense to store them as a tuple of two ints. But most of us don't think of integers like 23 as a tuple. We know that 23 is a rational number, but we don't like to think of it as being "23 divided by 1". So we included two different cases, one for whole numbers and one for rationals that we need to express as a fraction:

        structure Rational =
        struct
            datatype rational = Whole of int | Fraction of int * int
        end;
If we were providing a full-blown implementation, we'd include functions for adding, subtracting, multiplying and dividing such numbers. For our purposes, we'll implement just an add method as a way to explore these issues.

The signature for the add function is that it takes a tuple of rationals (rational * rational) and it returns a rational. Because there are two forms for a rational, we end up with four total cases for add:

        fun add(Whole(i), Whole(j)) = ?
        |   add(Whole(i), Fraction(c, d)) = ?
        |   add(Fraction(a, b), Whole(j)) = ?
        |   add(Fraction(a, b), Fraction(c, d)) = ?
It took us a while to work out the math, but together we filled in the following definitions:

        fun add(Whole(i), Whole(j)) = Whole(i + j)
        |   add(Whole(i), Fraction(c, d)) = Fraction(i * d + c, d)
        |   add(Fraction(c, d), Whole(i)) = Fraction(i * d + c, d)
        |   add(Fraction(a, b), Fraction(c, d)) = 
	        Fraction(a * d + c * b, b * d)
Someone pointed out that we should be reducing the result we get when we add two fractional numbers together. For example, if you were to add the rational numbers 1/8 with 1/4, our function will produce the rational number 12/32. Really this should be reduced to its lowest terms of 3/8.

How do we reduce a rational number? We find the greatest common divisor of the numerator and the denominator. Earlier in the quarter we used Euclid's algorithm to write gcd in this way:

        fun gcd(x, y) =
            if y = 0 then x
            else gcd(y, x mod y)
Using gcd, we were able to write a function to reduce a rational to its simplest form:

        fun reduce(Whole(i)) = Whole(i)
        |   reduce(Fraction(a, b)) =
                let val d = gcd(a, b)
                in Fraction(a div d, b div d)
                end
We then added a toString function:

        fun toString(Whole(i)) = Int.toString(i)
        |   toString(Fraction(a, b)) = Int.toString(a) ^ "/" ^ Int.toString(b)
I briefly mentioned that we have a potential problem that a client might construct a fraction that is not in reduced form. So we added the following function along with a comment that clients should use this function rather than the Fraction constructor to construct rational numbers:

        (* client: use this function instead of Fraction *)
        fun makeFraction(a, b) = reduce(Fraction(a, b));
Putting this all together, we ended up with the following structure:

        structure Rational =
        struct
            datatype rational = Whole of int | Fraction of int * int;
        
            fun gcd(x, y) =
        	if y = 0 then x
        	else gcd(y, x mod y);
        
            fun reduce(Whole(i)) = Whole(i)
            |   reduce(Fraction(a, b)) =
        	    let val d = gcd(a, b)
        	    in if b = d then Whole(a div d)
        	       else Fraction(a div d, b div d)
        	    end;
        
            fun add(Whole(i), Whole(j)) = Whole(i + j)
            |   add(Whole(i), Fraction(c, d)) = Fraction(i * d + c, d)
            |   add(Fraction(a, b), Whole(j)) = Fraction(a + j * b, b)
            |   add(Fraction(a, b), Fraction(c, d)) = 
        	    reduce(Fraction(a * d + b * c, b * d))
        
            fun toString(Whole(i)) = Int.toString(i)
            |   toString(Fraction(a, b)) = Int.toString(a) ^ "/" ^ Int.toString(b);
        
            (* client: use this function instead of Fraction *)
            fun makeFraction(a, b) = reduce(Fraction(a, b));
        end;

Stuart Reges
Last modified: Tue Apr 28 16:56:44 PDT 2009