content="Data-structures and algorithms in Standard ML. Notes from a lecture course given by Michael Fourman at the University of West Australia, Spring 1994." /> rel="start" type="text/html" href="/mfourman/index.html" title="Michael Fourman" /> Specification, Implementation, and Use

## Specification, Implementation, and Use

October 31, 2006

There are many models for the software life-cycle; this course is not the place to discuss them. However, they all include phases corresponding to the specification, implementation, and use, of a software component. In this note we introduce the SML module system, which supports these activities.

The module system has three parts: signatures, structures, and functors. In Practical 1, we have already seen examples of signatures, which specify an interface, and structures, which implement an interface. You were given a signature, A1, and asked to provide a structure, Answers1, implementing that signature, you used the structure PolyML, using a qualified name, PolyML.use, for one of its component functions. Functors provide a tool for separating implementation and use of a software module; they will be introduced in this note.

We use the example of implementing and using a new type, of complex numbers, to look at these constructs in more detail1. In Practical 2 you will perform a couple of similar exercises, implementing two new types: rational numbers, and approximate real numbers.

### Background

A complex number may be written in the form a + ib, as a sum of a real part, a, and an imaginary part ib. Here, a and b are real numbers. You don’t need to understand what a complex number is to understand this note (or to use complex numbers in many applications). We will just apply a few, straightforward rules for manipulating complex numbers. In particular, we can just treat i (the, supposedly mysterious, square root of -1) as a symbol to be manipulated according to the rules below.

(a + ib)+(c + id) = ((a + c) + i(b + d))
Subtraction
(a + ib)-(c + id) = ((a - c) + i(b - d))
Multiplication
(a + ib)*(c + id) = ((ac - bd) + i(bc + ad))
Division
(a + ib)(c + id) = ((a + ib) * (c - id)) * 1(c2 + d2)

Complex numbers may be pictured as points on the plane; a + ib corresponds to the point with cartesian coordinates (a,b). This representation is named after Descartes. We can also use polar coordinates for the same points; this gives a different representation of the complex numbers that we will name after another French mathematician, Argand. For this example, we will implement the arithmetic operations on complex numbers. We do this twice, using the two different representations.

### Specification

The interface we should provide is given by the following ML signature ComplexSig.

signature ComplexSig =
sig
type complex;

val descartes : {real    : real, imag     : real} -> complex
and argand    : {modulus : real, argument : real} -> complex

and ++ : complex * complex -> complex
and -- : complex * complex -> complex
and ** : complex * complex -> complex
and // : complex * complex -> complex
and X  : real    * complex -> complex
and == : complex * complex -> bool

and ~~      : complex -> complex
and modulus : complex -> real
and argument: complex -> real
and realpart: complex -> real
and imagpart: complex -> real
end;

The functions descartes and argand will construct complex numbers from explicit representations. The arithmetic operations, other than scalar multiplication, X, and unary minus, ~~, have been described earlier. The remaining functions, modulus, argument, realpart, and imagpart, allow us to construct an explicit representation of a complex number.

### Implementation

The structures Descartes provides an implementation of this signature, based on cartesian co-ordinates.

structure Descartes (*: ComplexSig*) =
struct
type complex = real * real;

fun r X (a,b) : complex = (r*a, r*b)

fun (a,b) ++ (c,d): complex = (a+c, b+d)
and (a,b) -- (c,d): complex = (a-c, b-d)
and (a,b) ** (c,d): complex = (a*c - b*d, b*c + a*d)
and (a,b) // (c,d): complex =
let val factor = 1.0/(c*c + d*d)
in
factor X (a,b) ** (c,~d)
end

and ~~(a,b) : complex = (~a,~b)

and (a,b) == (c,d) = (a=c) andalso (b=d)

val pi = 4.0 * arctan 1.0;

fun descartes{real, imag} = (real,imag)
and argand{modulus, argument} = modulus X (cos argument, sin argument)

and realpart (real, _) = real
and imagpart (_, imag) = imag

and modulus (r, i) = sqrt(r*r + i*i)
and argument(r, i) =
if r = 0.0
then if i < 0.0 then ~pi/2.0
else  pi/2.0
else arctan(i/r)
end;

 Figure 1: Cartesian representation of Complex numbers

An alternative representation, using polar coordinates to implement the same signature, is given by the structure Argand.

structure Argand : ComplexSig =
struct
type complex = real * real;

fun r X (a, m) : complex = (a, r*m)

fun (a, m) ** (a’, m’): complex = (a + a’, m * m’)
and (a, m) // (a’, m’): complex = (a - a’, m / m’)
and ~~(a, m) : complex = (a, ~m)

and (a,b) == (c,d) = (a=c) andalso (b=d)

val pi = 4.0 * arctan 1.0;

fun descartes {real=r, imag=i} =
let val argument =
if r = 0.0
then if i < 0.0 then ~pi/2.0
else  pi/2.0
else arctan(i/r)
val modulus = sqrt(r*r + i*i)
in
(argument, modulus)
end
and argand {modulus, argument} = (argument, modulus)

and realpart (a, m) = m * cos a
and imagpart (a, m) = m * sin a

and modulus  (a, m) = m
and argument (a, m) = a

fun x ++ y = let val r = realpart x + realpart y
and i = imagpart x + imagpart y
in
descartes{real = r, imag = i}
end
fun x -- y = x ++ (~~ y)

end;

 Figure 2: Polar representation of Complex numbers

A structure packages together a collection of types and values. We can access these either by opening the structure, which provides access to all its components, or by using long names, such as Argand.++. (Long names are never infix, so we would write Argand.++(x, y) where we might otherwise write x ++ y.)

### Use

When we use complex numbers, we should not be concerned to know which representation has been used to implement them. We now introduce functors the ML construct used to separate code that uses a given interface from the code that provides an implementation of that interface. As an example, we consider the implementation of 2-dimensional, complex vectors and matrices.

Our code for vectors and matrices will use the interface to complex numbers provided by ComplexSig. The specification of our vector package is provided by the signature VectorSig.

infix dot;

signature VectorSig =
sig
type scalar
type vector
type matrix

val vector: scalar * scalar -> vector
val matrix: vector * vector -> matrix

val scale : scalar * vector -> vector
val dot   : vector * vector -> scalar
val apply : matrix * vector -> vector
end;

The implementation will use the operations specified in ComplexSig. It uses the ML functor declaration. This allows us to write the code that we would write to implement VectorSig if we had already implemented a structure Complex: ComplexSig; it allows us to write this code before we implement ComplexSig — even before we choose which representation we will use to implement ComplexSig.

functor VECTOR( structure Complex : ComplexSig) : VectorSig  =
struct
open Complex;

type scalar = complex
type vector = complex * complex
type matrix = vector * vector

fun  vector (x,y) = (x,y)
fun  matrix (a,b) = (a,b)

fun scale (s,(a,b))   = vector (s ** a, s ** b)
fun (a,b) dot (a’,b’) = a ** a’ ++ b ** b’
fun apply ((a,b),v)   = vector (a dot v, b dot v)
end;

The functor VECTOR can be compiled before we implement complex numbers. Then, when we have an implementation, say Argand: ComplexSig, we can produce a structure, implementing VectorSig, by applying the functor, VECTOR, to the structure, Argand.

structure ArgandVec = VECTOR (structure Complex = Argand);

We can also apply the functor to Descartes to produce a vector package based on our other implementation of complex numbers.

This module system will allow us to build complex software from interchangeable components. It provides the tools we will need to experiment with different implementations of various datatypes.

### Pragmatics

Even for a relatively small example, such as the one presented in this note, managing the various pieces of code, compiling them in the right order, and making sure we recompile the appropriate parts when we make changes, quickly becomes tedious. PolyML provides a make utility to support this process. By placing the various code components in separate files, in a single directory, each with a name corresponding to the name of the signature, structure, or functor it defines, we can recompile a structure, S, by typing PolyML.make  "S";. If the sources of any of the components on which S depends have been changed, these components will be recompiled first.

The examples in this note will be found in the directory