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:
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:
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)We then added a toString function:
fun toString(Whole(i)) = Int.toString(i) | toString(Fraction(a, b)) = Int.toString(a) ^ "/" ^ Int.toString(b)This gives us our first version of the Rational structure:
(* initial version of the Rational structure that shows how we can group a datatype, its constructors, and some functions into a single unit. *) structure Rational = struct datatype rational = Whole of int | Fraction of int * int fun add(Whole i, Whole j) = Whole(i + j) | add(Whole i, Fraction(j, k)) = Fraction(j + k * i, k) | add(Fraction(j, k), Whole i) = Fraction(j + k * i, k) | add(Fraction(a, b), Fraction(c, d)) = Fraction(a * d + b * c, b * d) fun toString(Whole i) = Int.toString(i) | toString(Fraction(a, b)) = Int.toString(a) ^ "/" ^ Int.toString(b) endSomeone 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) endWe found that there was a further problem that sometimes when we reduce we end up with a denominator of 1, which should really be expressed as a whole number. So we added that as an extra case in reduce:
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;We also noticed that gcd had inconsistent behavior with negative numbers. For example, when we asked for:
gcd(~12, ~32)We got the answer from ML that this is ~4. It seems more appropriate to return a nonnegative number for gcd, so we added an extra case in the definition of gcd:
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)We can also end up with negatives when we call the Fraction constructor:
in:
reduce(Fraction(~12, ~32)); reduce(Fraction(~12, 32)); reduce(Fraction(12, ~32));We decided that if both numbers in the fraction are negative, then reduce should convert them to positive numbers and if only one is negative, then it should be the first one. One extra clause for b being negative takes care of both cases:
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) endThis new version produced the desired results:
- reduce(Fraction(~12, ~32)); val it = Fraction (3,8) : rational - reduce(Fraction(~12, 32)); val it = Fraction (~3,8) : rational - reduce(Fraction(12, ~32)); val it = Fraction (~3,8) : rationalI 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: please always construct fractions with this function *) fun makeFraction(a, b) = reduce(Fraction(a, b));Then we looked at the issue of having a value of 0 as a denominator. Division by zero is undefined, so we decided to introduce an exception and to include this in our makeFraction function:
exception NotARational; fun makeFraction(a, b) = if b = 0 then raise NotARational else reduce(Fraction(a, b));Putting this all together, we ended up with this second version of the Rational structure:
(* Second version of Rational that includes gcd and reduce to reduce fractions to their lowest form and a makeFraction function that allows us to guarantee our invariants (no 0 denominator, fractions always reduced). *) structure 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 = 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