In order to implement the data-type Rational we need a facility to glue two integers in to a compound value. We may do this in ML using the primitive declaration datatype and a constructor in the following way: 4datatype declaration datatype number = ratify of int*int; 5 The above declaration defines the data-type number to be a compound value created with the constructor ratify. Note that the choice of the name of the constructor is arbitrary and we could choose any name for the constructor. The constructor ratify accepts two input arguments and returns a compound value which is a pair with the two input arguments as parts. Thus, if the two input integers are a and b then the compound value is ratify(a,b). The compound value can be un-glued by a powerful ML concept called pattern matching. The ML declarations 6pattern matching fun numer(ratify(a,_)) = a; fun denom(ratify(_,b)) = b; 7 define numer and denom to be functions that accept compound items created with the constructor ratify as input and return the components a and b respectively as output. The mechanism for extracting the components a and b from the compound item is called pattern matching. The notation ratify(a,_) means that we do not care about the second argument in the pattern matching.
Armed with the above, we can now define an ML module called RAT1 which is our first implementation of the abstract data type Rational.
8Module RAT1 structure RAT1 : Rational = struct datatype number = ratify of int*int;
exception DenomIsZero;
fun gcd(a,b) = if b = 0 then a else gcd(b,a mod b);
fun makerat(n:int,d:int) : number = if d = 0 then raise DenomIsZero else let val pn = abs(n); val pd = abs(d); val sign = if n = 0 then 0 else (n div pn) * (d div pd); val g = gcd(pn,pd) in ratify(sign * pn div g,pd div g) end;
fun numer(ratify(x,_)) = x; fun denom(ratify(_,y)) = y;
fun plus(a,b) = let val x = numer(a)*denom(b) + numer(b)*denom(a); val y = denom(a)*denom(b); in makerat(x,y) end;
fun minus(a,b) = ... fun mult(a,b) = ... fun div(a,b) = ... fun equalto(a,b) = ... end; 9
Note that the ML module RAT1 uses the internal function gcd to reduce a rational number to its canonical form. It also uses functions numer and denom for the internal representation of the data type. However, since these functions are not a part of the signature Rational, they are not visible outside the module RAT1, and, consequently, an usage like RAT1.gcd(4,6) is illegal. Only those components of a module which are explicitly specified in the signature of the abstract data type are available for use from outside.
We have thus implemented the data-type for rational numbers using several abstraction barriers. At the lowest level is the pairing of integers using the primitive datatype and the constructor facility in ML . At the next higher level are our functions makerat, numer and denom. Using these functions we have defined, at a still higher level, our functions for manipulating rational numbers. Any user program at a higher level may now simply define variables of the type rational (RAT1.number), construct rational numbers using the function makerat and use the functions for carrying out arithmetic on rationals directly. The overall hierarchy is illustrated in Figure 6.2.
Thus, at the interface to the highest level of any program using rational numbers, only the functions RAT1.makerat, RAT1.plus, RAT1.minus, RAT1.mult, RAT1.div and RAT1.equalto need to be available. The rest of the detail of the implementation may be completely hidden from an user program. This is achieved through the signature declaration.The above idea of hiding the details of the implementation of an abstract data type is key to modular programming. After all, in order to drive a car we need to know only the methods available - the use of the steering, the brake, the indicators 6.1, the clutch and gear changing. We definitely do not want to know about the details of the transmission system, the viscocity of the brake fluid etc. These issues are relevant when we are designing a car, exactly in the same way the internal representation of the data type is relevant to the designer of the data-type module. This clear cut separations of concerns is key to succesful development of large programs.
We may even change the underlying representation of rational numbers without affecting any computation at the higher levels. For example, we may consider a totally different, though somewhat inefficient, implementation of our functions makerat, numer and denom. If we consider only positive rational numbers, we may define the function makerat as
16Module RAT2 structure RAT2 : Rational = struct type number = int;
exception DenomIsZero;
fun gcd(a,b) = if b = 0 then a else gcd(b,a mod b);
fun power(x,n) = ...
fun makerat(n:int,d:int) : number = if d = 0 then raise DenomIsZero else let val pn = abs(n); val pd = abs(d); val sign = if n = 0 then 0 else (n div pn) * (d div pd); val g = gcd(pn,pd) in if sign = 0 then power(2,pn div g)*power(3,pd div g) else sign * power(2,pn div g)*power(3,pd div g) end;
fun numer(r : number) = let val sign = r div abs(r); fun niter(r,i) = if (r mod 2) <> 0 then i else niter(r div 2,i+1) in sign*niter(abs(r),0) end;
fun denom(r : number) = let fun niter(r,i) = if (r mod 3) <> 0 then i else niter(r div 3,i+1) in niter(abs(r),0) end;
fun plus(a,b) = let val x = numer(a)*denom(b) + numer(b)*denom(a); val y = denom(a)*denom(b); in makerat(x,y) end;
fun minus(a,b) = ... fun mult(a,b) = ... fun div(a,b) = ... fun equalto(a,b) = ... end; 17