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 also talked about what reduce should do when given negative values, as 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. So we modified reduce to have this behavior:
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) : rationalThen I discussed in more detail the idea of a signature. In Java we identify each part of a class as either public or private. That means that you'd have to read through the entire class to see which elements of the class are available to clients. That can be inconvenient. It's nice for a client to see a concise list of exactly what is publicly available. I said that Java does have something like this and someone mentioned Javadoc. That's exactly right. If you create the Javadoc comments for a class, you'll get a listing of just the client view of the class, showing you just the parts of the class that are exposed to clients.
In ML this is done more explicitly. A signature is a language construct that is similar to a Javadoc listing. It indicates exactly what a client has access to. The general form of a signature is as follows:
signature RATIONAL = sig datatype rational = Whole of int | Fraction of int * int exception NotARational val add : rational * rational -> rational val toString : rational -> string endThere 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 ... endWe 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.
Someone asked whether a signature can be implemented by multiple structures and I said yes. For example, there is a signature called INTEGER that several different structures implement (different flavors of integer, as with simple ints versus infinite ints). Someone else asked whether a structure could implement multiple signatures. I said I wasn't sure, but I hadn't seen any examples of that. I believe the more common practice is to define multiple signatures that extend each other.
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. So we wrote a new function called makeFraction that can be used in place of the Fraction constructor to make sure that the value is stored in a proper form. We also included code to check for a denominator of 0:
fun makeFraction(a, 0) = raise NotARational | makeFraction(a, b) = reduce(Fraction(a, b));We could then encourage the client of the class to call this function instead of calling the Fraction constructor. But still the best we can say is something like the following:
(* clients of Rational: use makeFraction instead of Fraction *) (* invariant: for any Fraction(a, b), b > 0 if they called makeFraction *)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 add : rational * rational -> rational val toString : rational -> string endWe 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 add : rational * rational -> rational val toString : rational -> string endThis 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, so we had to rewrite these variable declarations:
val x = Rational.Whole(23); val y = Rational.makeFraction(27, 8); val z = Rational.add(x, y);to be:
val x = Rational.makeFraction(23, 1); val y = Rational.makeFraction(27, 8); val z = Rational.add(x, y);At this point we ran out of time, so we had to save this for Wednesday's lecture.