CSE341 Notes for Wednesday, 10/28/09

I spent some time discussing modules. I mentioned that the languages that are most familiar to us today come from Algol, which was created in the 1960's. Algol had many great features (what was generally called "structured programming") many of which could be described as "modular" programming. The key ideas behind modularity are:

Even though Algol had these great ideas of modularity, it only had one level of modularity. The concept of a module is to have a collection of variables, types, and subprograms that constitute a module. That means grouping them together in some way, providing information hiding, and having a way to specify interfaces. Algol did not have modules.

From Algol we got Pascal, C, and C++, none of which have a good module capability. We got Ada as well, which did have a robust module construct, but it was unpopular because it had been invented by the Department of Defense.

In the late 1980's people we started seeing languages like Modula 2, which was invented by the same researcher who invented Pascal. The main difference was that Modula 2 had modules. When Java came along soon afterwards, they included something like modules, although in a funny way.

Java has packages, which are collections of classes and interfaces. There are also mechanisms for information hiding within a package, which is good. But we normally think of a module as including functions and variables as well. In Java we collect those things together by putting them in a class. What would normally be free-standing functions and variables in a module become static elements of a class. The Math class in Java is a classic example. It has static variables Math.PI and Math.E and static methods like Math.sin, Math.cos, and Math.abs.

We briefly discussed some of the benefits of modules:

Then we returned to the example of the Rational structure from the previous lecture. For the third version, I incorporated a signature. A signature is a language construct that is somewhat similar to an interface in Java. It allows you to describe language elements abstractly, without revealing the details of the implementation. And like an interface, it indicates exactly what a client has access to. The general form of a signature is as follows:

signature <name> = sig <definitions> end You are more limited in what you can include in a signature. You can generally include just descriptions of the form that various elements take. For example, you can't define any concrete functions. Instead, you include descriptions of the function type using the keyword val:

        signature RATIONAL =
        sig
            datatype rational = Whole of int | Fraction of int * int
            exception NotARational
            val add : rational * rational -> rational
            val toString: rational -> string
            val makeFraction: int * int -> rational
        end
There is a convention in ML to use all uppercase letters for a signature name and capitalized words for structure names. That allows us to reuse the same word, as in this example of a signature called RATIONAL implemented by a structure called Rational. But this is just a common convention. There is no requirement that these names be related to each other.

Given such a signature, you can include a notation in the header of a structure to indicate that you want to restrict access to just those things listed in the signature. We do so by using the characters ":>" and including the name of the signature when we define a structure, as in:

        structure Rational :> RATIONAL =
        struct
            ...
        end
We found several interesting things when we loaded this version of the file into ML. The functions gcd and reduce were no longer visible. In Java we would have declared them to be private. Here they are implicitly private because they are not mentioned in the signature. Only those things mentioned in the signature are visible to clients. We found this was true even if we opened the structure. We simply couldn't see the gcd and reduce functions. This is a very useful technique to hide the internal details of an implementation and to avoid cluttering up the namespace.

The notation "Rational :> RATIONAL" is similar to Java's notion of having a class that implements an interface. Each element mentioned in the signature has to be included in the structure. For example, if the signature indicates that a function called add should exist, then the structure must include such a function. In the Ullman book he shows the less common version of this with a simple colon: "Rational : RATIONAL". He later describes the ":>" as a variation. It is more common to use the ":>" restriction, which is known as an opaque implementation. The distinction is not terribly important, so I said we wouldn't spend time talking about it.

Then I turned to the question of making sure that we have good rational numbers. We know that our add function will return an answer in lowest terms, but a client might construct a rational number like 12/32. So should toString call reduce? Should all of our functions call reduce? A better approach is to try to guarantee an invariant that any rational number is in a proper form. Our makeFraction function is supposed to take care of this, but we don't want to rely on the "please client" comment that we included in the file.

Obviously we'd like to have a stronger guarantee. ML gives us a way to achieve this. In the signature, we currently list the details of the type:

        signature RATIONAL =
        sig
            datatype rational = Whole of int | Fraction of int * int
            exception NotARational
            val makeFraction: int * int -> rational
            val add : rational * rational -> rational
            val toString : rational -> string
        end
We can instead just mention that a rational type will be defined without specifying the details of how it is defined:

        signature RATIONAL =
        sig
            type rational
            exception NotARational
            val makeFraction: int * int -> rational
            val add : rational * rational -> rational
            val toString : rational -> string
        end
This is known as an abstract type. When we use this signature, a client cannot see the Fraction constructor. Unfortunately, a client also can't see the Whole constructor, which would require a client to say things like:

        val x = Rational.makeFraction(23, 1);
        val y = Rational.makeFraction(27, 8);
        val z = Rational.add(x, y);
This is fairly easy to fix. We can simply add a signature for the Whole constructor in the RATIONAL signature:

        signature RATIONAL =
        sig
            type rational
            exception NotARational
            val makeFraction : int * int -> rational
            val Whole : int -> rational
            val add : rational * rational -> rational
            val toString : rational -> string
        end
We don't have to expose the details of the rational type to let ML and clients know that there is something called Whole that allows them to construct a rational number from a single int. This allowed us to again write client code like the following:

        val x = Rational.Whole(23);
        val y = Rational.makeFraction(27, 8);
        val z = Rational.add(x, y);
We these changes, we have guaranteed that clients must use either Whole or makeFraction to construct a rational number. That means that we have the invariant we were looking for:

        (* invariant: for any Fraction(a, b), b > 0 and gcd(a, b) = 1 *)
We still need to call reduce in the add function because the arithmetic involved in add can lead to a fraction that needs to be reduced, but we don't have to call reduce in functions like toString because we know that it's not possible for a client to construct a rational number that violates our invariant.

Here is the complete fourth version of the Rational structure:

(* Fourth version of Rational that further restricts the signature so that
   the Fraction constructor is not exposed--finally we can guarantee
   invariants because the client must use makeFraction *)

signature RATIONAL =
sig
   type rational
   exception NotARational
   val makeFraction : int * int -> rational
   val add : rational * rational -> rational
   val toString : rational -> string
   val Whole : int -> rational
end
    
structure Rational :> RATIONAL =
struct
   datatype rational = Whole of int | Fraction of int * int
   exception NotARational

   fun gcd(x, y) =
       if x < 0 orelse y < 0 then gcd(abs(x), abs(y))
       else 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 < 0 then reduce(Fraction(~a, ~b))
               else if b = d then Whole(a div d)
	       else Fraction(a div d, b div d)
	    end;

    (* client: please always construct fractions with this function *)
    fun makeFraction(a, b) = 
       if b = 0 then raise NotARational
       else reduce(Fraction(a, b))

    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 + c * b, b * d));

   fun toString(Whole i) = Int.toString(i)
   |   toString(Fraction(a, b)) = Int.toString(a) ^ "/" ^ Int.toString(b)
end
I mentioned that using a signature with an abstract type, you can use a completely different internal implementation and the client would never even know it. For example, here is an alternative implementation of the signature that implements rationals as a tuple of two ints:

(* Fifth version of Rational that reimplements the type using an int * int.
   This change would be invisible (opaque) to a client of the structure. *)

signature RATIONAL =
sig
   type rational
   exception NotARational
   val makeFraction : int * int -> rational
   val add : rational * rational -> rational
   val toString : rational -> string
   val Whole : int -> rational
end
    
structure Rational :> RATIONAL =
struct
   type rational = int * int
   exception NotARational

   fun gcd(x, y) =
       if x < 0 orelse y < 0 then gcd(abs(x), abs(y))
       else if y = 0 then x
       else gcd(y, x mod y);

    fun reduce(a, b) =
        let val d = gcd(a, b)
        in if b < 0 then reduce(~a, ~b)
           else (a div d, b div d)
        end;
        
    fun makeFraction(a, 0) = raise NotARational
    |   makeFraction(a, b) = reduce(a, b);
        
    fun Whole(a) = (a, 1);

    fun add((a, b), (c, d)) = reduce(a * d + c * b, b * d);
        
    fun toString(a, b) =
        if b = 1 then Int.toString(a)
        else Int.toString(a) ^ "/" ^ Int.toString(b)
end
Here we use a type definition rather than a datatype definition because we are introducing a type synonym rather than a new type. It also means that we have to define Whole as a function rather than a constructor. This new structure provides the same functionality to a client as the original and the client would have no way of telling them apart because the signature uses an abstract type. This is a powerful and useful mechanism.

As a final example, I included a version that uses this new representation of a rational as a tuple and that uses a lazy approach rather than an eager approach to reducing a pair to its lowest terms. The previous versions call reduce both in makeFraction and in add. Instead, we can wait until toString is called to call reduce because that's the first point in time when the client would notice that we hadn't reduced:

(* Sixth version of Rational that does a "lazy" reduce by only reducing
   in toString *)

signature RATIONAL =
sig
   type rational
   exception NotARational
   val makeFraction : int * int -> rational
   val add : rational * rational -> rational
   val toString : rational -> string
   val Whole : int -> rational
end
    
structure Rational :> RATIONAL =
struct
   type rational = int * int
   exception NotARational

   fun gcd(x, y) =
       if x < 0 orelse y < 0 then gcd(abs(x), abs(y))
       else if y = 0 then x
       else gcd(y, x mod y);

    fun reduce(a, b) =
        let val d = gcd(a, b)
        in if b < 0 then reduce(~a, ~b)
           else (a div d, b div d)
        end;
        
    fun makeFraction(a, 0) = raise NotARational
    |   makeFraction(a, b) = (a, b);
        
    fun Whole(a) = (a, 1);

    fun add((a, b), (c, d)) = (a * d + c * b, b * d);
        
    fun toString(a, b) =
	let val (a2, b2) = reduce(a, b)
	in if b2 = 1 then Int.toString(a2)
           else Int.toString(a2) ^ "/" ^ Int.toString(b2)
	end;
end
The key point is not whether eager versus lazy computation is better. The key point is that the client can't tell the difference, which means that the implementor has the flexibility to choose either approach.

Then I briefly revisited the idea of static versus dynamic properties of programs. I have been keeping a 2-column list of various properties we have discussed:

        static                    dynamic
        compile-time              run-time
        compilers                 interpreters
        static type checking      dynamic type checking
        lexical scope             dynamic scope
                                  flow of control
I briefly discussed the notion of dynamic scope. Consider, for example, 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 from the lexical scope rules of Java that the reference to x in the method called "one" will be a reference to the global x, producing the following output:
        6
        12
        5
        24
        2
There is a different scheme known as dynamic scope in which you pay attention to the invocation of the various methods. Each method call introduces a new scope. You'd get a different result using dynamic scoping rules. In fact, with dynamic scope, the reference to x in method one ends up refering to each of the variables called x in this program.

I showed a shell script that is equivalent to the 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
        }
        
        main
Shell scripts use dynamic scope, so this code produces different output:

        6
        10
        10
        4
        4
Below is a diagram of the different dynamic scopes that are produced when this program executes:

         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 mentioned that I'm not expecting everyone to understand how dynamic scope works. The key point is that in a dynamic scope scheme, what matters is the sequence of functions calls, which is a dynamic property of the program.


Stuart Reges
Last modified: Wed Oct 28 16:09:11 PDT 2009