SML chapter3
SML chapter3
Lists
Chapter outline
This chapter describes how to program with lists in Standard ML. It
presents several examples that would normally involve arrays, such as matrix
operations and sorting.
The chapter contains the following sections:
Introduction to lists. The notion of list is introduced. Standard ML operates
on lists using pattern-matching.
Some fundamental list functions. A family of functions is presented. These
are instructive examples of list programming, and are indispensable when tack-
ling harder problems.
Applications of lists. Some increasingly complicated examples illustrate the
variety of problems that can be solved using lists.
The equality test in polymorphic functions. Equality polymorphism is intro-
69
70 3 Lists
duced and demonstrated with many examples. These include a useful collection
of functions on finite sets.
Sorting: A case study. Procedural programming and functional programming
are compared in efficiency. In one experiment, a procedural program runs only
slightly faster than a much clearer functional program.
Polynomial arithmetic. Computers can solve algebraic problems. Lists are
used to add, multiply and divide polynomials in symbolic form.
Introduction to lists
A list is a finite sequence of elements. Typical lists are [3,5,9] and
["fair","Ophelia"]. The empty list, [], has no elements. The order of
elements is significant, and elements may appear more than once. For instance,
the following lists are all different:
[3,4] [4,3] [3,4,3] [3,3,4]
The elements of a list may have any type, including tuples and even other lists.
Every element of a list must have the same type. Suppose this type is τ ; the type
of the list is then τ list. Thus
[(1,"One"), (2,"Two"), (3,"Three")] : (int*string) list
[ [3.1], [], [5.7, ˜0.6] ] : (real list) list
The empty list, [], has the polymorphic type α list. It can be regarded as having
any type of elements.
Observe that the type operator list has a postfix syntax. It binds more tightly
than × and →. So int × string list is the same type as int × (string list), not
(int × string)list. Also, int list list is the same type as (int list)list.
nil = []
9 :: [] = [9]
5 :: [9] = [5, 9]
3 :: [5, 9] = [3, 5, 9]
Observe that the elements are taken in reverse order. The list [3, 5, 9] can be
written in many ways, such as 3 :: (5 :: (9 :: nil )), or 3 :: (5 :: [9]), or 3 :: [5, 9].
To save writing parentheses, the infix operator ‘cons’ groups to the right. The
notation [x1 , x2 , . . . , xn ] stands for x1 :: x2 :: · · · :: xn :: nil . The elements may
be given by expressions; a list of the values of various real expressions is
[ Math.sin 0.5, Math.cos 0.5, Math.exp 0.5 ];
> [0.479425539, 0.877582562, 1.64872127] : real list
List notation makes a list with a fixed number of elements. Consider how to
build the list of integers from m to n:
[m, m + 1, . . . , n]
First compare m and n. If m > n then there are no numbers between m and n;
the list is empty. Otherwise the head of the list is m and the tail is [m+1, . . . , n].
Constructing the tail recursively, the result is obtained by
m :: [m + 1, . . . , n].
Lists in other languages. Weakly typed languages like Lisp and Prolog rep-
resent lists by pairing, as in (3, (5, (9, "nil"))). Here "nil" is some
end marker and the list is [3, 5, 9]. This representation of lists does not work in ML
because the type of a ‘list’ would depend on the number of elements. What type could
upto have?
ML’s syntax for lists differs subtly from Prolog’s. In Prolog, [5|[6]] is the same
list as [5,6]. In ML, [5::[6]] is the same list as [[5,6]].
72 3 Lists
declares a function to take the product of a list of numbers — but only if there
are exactly three of them!
List operations are usually defined by recursion, treating several cases. What
is the product of a list of integers?
The function consists of two clauses separated by a vertical bar (|). Each clause
treats one argument pattern. There may be several clauses and complex patterns,
provided the types agree. Since the patterns involve lists, and the result can be
the integer 1, ML infers that prod maps a list of integers to an integer.
prod [2,3,5];
> 30 : int
Empty versus non-empty is the commonest sort of case analysis for lists. Find-
ing the maximum of a list of integers requires something different, for the empty
list has no maximum. The two cases are
• The maximum of the one-element list [m] is m.
• To find the maximum of a list with two or more elements [m, n, . . .],
remove the smaller of m or n and find the maximum of the remaining
numbers.
This gives the ML function
fun maxl [m] : int = m
| maxl (m::n::ns) = if m>n then maxl (m::ns)
else maxl (n::ns);
> ***Warning: Patterns not exhaustive
> val maxl = fn : int list -> int
3.2 Operating on a list 73
Note the warning message: ML detects that maxl is undefined for the empty
list. Also, observe how the pattern m :: n :: ns describes a list of the form
[m, n, . . .]. The smaller element is dropped in the recursive call.
The function works — except for the empty list.
maxl [ ˜4, 0, ˜12];
> 0 : int
maxl [];
> Exception: Match
An exception, for the time being, can be regarded as a run-time error. The func-
tion maxl has been applied to an argument for which it is undefined. Normally
exceptions abort execution. They can be trapped, as we shall see in the next
chapter.
Intermediate lists. Lists are sometimes generated and consumed within a com-
putation. For instance, the factorial function has a clever definition using prod
and upto:
fun factl (n) = prod (upto (1,n));
> val factl = fn : int -> int
factl 7;
> 5040 : int
This declaration is concise and clear, avoiding explicit recursion. The cost of
building the list [1, 2, . . . , n] may not matter. However, functional programming
should facilitate reasoning about programs. This does not happen here. The
trivial law
factl (m + 1) = (m + 1) × factl (m)
The next step is unclear because the recursion in upto follows its first argument,
not the second. The honest recursive definition of factorial seems better.
Strings and lists. Lists are important in string processing. Most functional lan-
guages provide a type of single characters, regarding strings as lists of charac-
ters. With the new standard library, ML has acquired a character type — but it
does not regard strings as lists. The built-in function explode converts a string
to a list of characters. The function implode performs the inverse operation,
joining a list of characters to form a string.
74 3 Lists
explode "Banquo";
> [#"B", #"a", #"n", #"q", #"u", #"o"] : char list
implode it;
> "Banquo" : string
The function is polymorphic: testing whether a list is empty does not examine its
elements. The underscores ( ) in the second pattern take the place of components
whose values are not needed in the clause. These underscores, called wildcard
patterns, save us from inventing names for such components.
The function hd. This function returns the head (the first element) of a non-
empty list:
fun hd (x ::_) = x ;
> ***Warning: Patterns not exhaustive
> val hd = fn : ’a list -> ’a
This pattern has a wildcard for the tail, while the head is called x . Since there
is no pattern for the empty list, ML prints a warning. It is a partial function
like maxl .
Here we have a list of lists. Its head is a list and the head of that is an integer.
Each use of hd removes one level of brackets.
3.3 Testing lists and taking them apart 75
The function tl. This returns the tail of a non-empty list. The tail, remember, is
the list consisting of the all elements but the first.
fun tl (_::xs) = xs;
> ***Warning: Patterns not exhaustive
> val tl = fn : ’a list -> ’a list
Like hd, this is a partial function. Its result is always another list:
tl ["Out","damned","spot!"];
> ["damned", "spot!"] : string list
tl it;
> ["spot!"] : string list
tl it;
> [] : string list
tl it;
> Exception: Match
If you prefer this version of prod, you might as well give up ML for Lisp. For
added clarity, Lisp primitives have names like CAR and CDR. Normal people find
pattern-matching more readable than hd and tl. A good ML compiler analyses
the set of patterns to generate the best code for the function. More importantly,
the compiler prints a warning if the patterns do not cover all possible arguments
of the function.
Exercise 3.1 Write a version of maxl using null , hd and tl , instead of pattern-
matching.
The function length. The length of a list can be computed by a naı̈ve recursion:
fun nlength [] = 0
| nlength (x ::xs) = 1 + nlength xs;
> val nlength = fn : ’a list -> int
Its type, α list → int, permits nlength to be applied to a list regardless of the
type of its elements. Let us try it on a list of lists:
nlength[[1,2,3], [4,5,6]];
> 2 : int
The ones pile up, wasting space proportional to the length of the list, and could
easily cause execution to abort. Much better is an iterative version of the func-
tion that accumulates the count in another argument:
local
fun addlen (n, []) = n
| addlen (n, x ::l ) = addlen (n+1, l )
in
fun length l = addlen (0,l )
end;
> val length = fn : ’a list -> int
length (explode"Throw physic to the dogs!");
> 25 : int
The function addlen adds the length of a list to another number, initially 0.
Since addlen has no other purpose, it is declared local to length. It executes
3.4 List processing by numbers 77
as follows:
The function take. Calling take(l ,i ) returns the list of the first i elements of l :
fun take ([], i) = []
| take (x ::xs, i) = if i>0 then x ::take(xs, i-1)
else [];
> val take = fn : ’a list * int -> ’a list
take (explode"Throw physic to the dogs!", 5);
> [#"T", #"h", #"r", #"o", #"w"] : char list
The function drop. The list drop(l ,i ) contains all but the first i elements of l :
fun drop ([], ) = []
| drop (x ::xs, i) = if i>0 then drop (xs, i-1)
else x ::xs;
> val drop = fn : ’a list * int -> ’a list
Exercise 3.3 What do take(l ,i ) and drop(l ,i ) return when i > length(l ),
and when i < 0? (The library versions of take and drop would raise excep-
tions.)
Exercise 3.4 Write a function nth(l ,n) to return the nth element of l (where
the head is element 0).
The append operation. Append puts the elements of one list after those of an-
other list:
[x1 , . . . , xm ] @ [y1 , . . . yn ] = [x1 , . . . , xm , y1 , . . . yn ]
3.5 Append and reverse 79
What sort of recursion accomplishes this? The traditional name append suggests
that the action takes place at the end of a list, but lists are always built from the
front. The following definition, in its essentials, dates to the early days of Lisp:
infixr 5 @;
fun ([] @ ys) = ys
| ((x ::xs) @ ys) = x :: (xs@ys);
> val @ = fn : ’a list * ’a list -> ’a list
Its type, α list × α list → α list, accepts any two lists with the same element
type — say, lists of strings and lists of lists:
["Why", "sinks"] @ ["that", "cauldron?"];
> ["Why", "sinks", "that", "cauldron?"] : string list
[[2,4,6,8], [3,9]] @ [[5], [7]];
> [[2, 4, 6, 8], [3, 9], [5], [7]] : int list list
The last three steps put the elements from the first list on to the second. As
with take, the cost of building the result exceeds that of the deep recursion; an
iterative version is not needed. The cost of evaluating xs @ ys is proportional
to the length of xs and is completely independent of ys. Even xs @ [] makes a
copy of xs.
In Pascal and C you can implement lists using pointer types, and join them
by updating the last pointer of one list to point towards another. Destructive
updating is faster than copying, but if you are careless your lists could end up
in knots. What happens if the two lists happen to be the same pointer? ML
lists involve internal pointers used safely. If you like living dangerously, ML has
explicit pointer types — see Chapter 8.
The function rev. List reversal can be defined using append. The head of the list
becomes the last element of the reversed list:
80 3 Lists
fun nrev [] = []
| nrev (x ::xs) = (nrev xs) @ [x ];
> val nrev = fn : ’a list -> ’a list
This is grossly inefficient. If nrev is given a list of length n > 0, then append
calls cons (::) exactly n − 1 times to copy the reversed tail. Constructing the
list [x ] calls cons again, for a total of n calls. Reversing the tail requires n − 1
more conses, and so forth. The total number of conses is
n(n + 1)
0 + 1 + 2 + ··· + n = .
2
This cost is quadratic: proportional to n 2 .
We have already seen, in rtake, another way of reversing a list: repeatedly
move elements from one list to another.
fun revAppend ([], ys) = ys
| revAppend (x ::xs, ys) = revAppend (xs, x ::ys);
> val revAppend = fn : ’a list * ’a list -> ’a list
Exercise 3.7 Show the computation steps to reverse the list [1, 2, 3, 4] using
nrev and then rev .
3.6 Lists of lists, lists of pairs 81
The function concat. This function makes a list consisting of all the elements of
a list of lists:
fun concat [] = []
| concat(l ::ls) = l @ concat ls;
> val concat = fn : ’a list list -> ’a list
concat [["When","shall"], ["we","three"], ["meet","again"]];
> ["When", "shall", "we", "three", "meet", "again"]
> : string list
The copying in l @ concat ls is reasonably fast, for l is usually much shorter than
concat ls.
The function zip. This function pairs corresponding members of two lists:
zip([x1 , . . . , xn ], [y1 , . . . , yn ]) = [(x1 , y1 ), . . . , (xn , yn )]
If the two lists differ in length, let us ignore surplus elements. The declaration
requires complex patterns:
fun zip(x ::xs,y::ys) = (x ,y) :: zip(xs,ys)
| zip _ = [];
> val zip = fn : ’a list * ’b list -> (’a*’b) list
The second pattern in the definition of zip, with its wildcard, matches all possi-
bilities. But it is only considered if the first pattern fails to match. ML considers
a function’s patterns in the order given.
The function unzip. The inverse of zip, called unzip, takes a list of pairs to a
pair of lists:
unzip[(x1 , y1 ), . . . , (xn , yn )] = ([x1 , . . . , xn ], [y1 , . . . , yn ])
Building two lists simultaneously can be tricky in functional languages. One
approach uses an extra function:
fun conspair ((x ,y), (xs,ys)) = (x ::xs, y::ys);
fun unzip [] = ([],[])
| unzip(pair ::pairs) = conspair (pair , unzip pairs);
A let declaration, where pattern-matching takes apart the result of the recursive
call, eliminates the function conspair :
82 3 Lists
An iterative function can construct several results in its arguments. This is the
simplest way to unzip a list, but the resulting lists are reversed.
fun revu nzip([], xs, ys) = (xs,ys)
| revu nzip((x ,y)::pairs, xs, ys) =
revu nzip(pairs, x ::xs, y::ys);
Lists and the standard library. The standard library provides most of the func-
tions described above. Append (the infix @), reverse (rev ), null , hd , tl and
length are available at top level. List provides take, drop and concat, among others.
ListPair provides zip and unzip.
Please use the library versions of these functions. Those given here omit error han-
dling. The library versions respond to erroneous inputs by raising exceptions such as
List.Empty (if you request the head of the empty list) and Subscript (if you attempt
to take more elements than exist). Library versions will also be tuned for efficiency.
Exercise 3.8 Compare the following function with concat, considering its ef-
fect and efficiency:
fun f [] = []
| f ([]::ls) = f (ls)
| f ((x ::l )::ls) = x :: f (l ::ls);
Exercise 3.9 Give an equivalent definition of zip that does not depend upon
the order in which patterns are considered.
Exercise 3.10 Is rev (rtake(l ,i ,[])) more efficient than take(l ,i )? Con-
sider all the costs involved.
Applications of lists
This section demonstrates how lists can perform sophisticated tasks, like
binary arithmetic and matrix operations. Feel free to skip the harder ones if you
like.
Two examples from the classic book A Discipline of Programming (Dijkstra,
1976) are also solved. Dijkstra presents programs in all their ‘compelling and
deep logical beauty.’ His programs use arrays; do lists possess greater beauty?
3.7 Making change 83
The function definition could hardly be more intuitive. If the target amount is
zero, no coins are required; if the largest coin value c is too large, discard it;
otherwise use it and make change for the amount less c.
Let us declare ML identifiers for British and U.S. coin values:
Making change is less trivial than it first appears. Suppose the only coin values
we have are 5 and 2?
change([5,2], 16);
> Exception: Match
The ‘patterns not exhaustive’ warning has disappeared; the function considers
all cases. Given an impossible problem, it returns the empty list instead of rais-
ing an exception:
allChange([], [10,2], 27);
> [] : int list list
There are 25 ways of making change for 16 pence! At first sight, this approach
looks untenable. To control the exponential growth in solutions we can use lazy
evaluation, generating only the solutions required (see Section 5.14).
Further reading. Making change is much harder than it first appears, in the
general case. It is closely related to the subset-sum problem, which is NP-
complete. This means it is highly unlikely that there exists an efficient algorithm to
decide whether or not a given set of coins can be used to express a given sum. Cormen
et al. (1990) discuss algorithms for finding approximate solutions.
Exercise 3.12 The change functions expect coinvals to consist of strictly de-
creasing positive integers. What happens if this precondition is violated?
Addition. If you have forgotten the rules for binary sums, have a look at the
binary version of 11 + 30 = 41:
11110
+ 1011
101001
Addition works from right to left. The two bits plus any carry (from the right)
give a sum bit for this position and a carry to the left. Right to left is the wrong
direction for lists; the head of a list is its leftmost element. So the bits will be
kept in reverse order.
The two binary numerals may have unequal lengths. If one bit list terminates
then the carry must be propagated along the other bit list.
fun bincarry (0, ps) = ps
| bincarry (1, []) = [1]
| bincarry (1, p::ps) = (1-p) :: bincarry(p, ps);
> ***Warning: Patterns not exhaustive
> val bincarry = fn : int * int list -> int list
Yes, patterns may contain constants: integers, reals, booleans and strings. Func-
tion bincarry can propagate a carry of 0 or 1, the only sensible values. It is
undefined for others.
The binary sum is defined for two bit lists and a carry. When either list termi-
nates, bincarry deals with the other. If there are two bits to add, their sum and
carry are computed:
fun binsum (c, [], qs) = bincarry (c,qs)
| binsum (c, ps, []) = bincarry (c,ps)
| binsum (c, p::ps, q::qs) =
((c+p+q) mod 2) :: binsum((c+p+q) div 2, ps, qs);
> val binsum = fn
> : int * int list * int list -> int list
Let us try 11 + 30 = 41, remembering that the bits are kept in reverse order:
86 3 Lists
With a little more effort, structure Bin can be made to match signature ARITH
of Section 2.22. This would give the operators for binary numerals precisely the
same interface as those for complex numbers. It would include binary arithmetic
in our collection of structures that can be used in generic arithmetic packages.
But binary arithmetic is quite different from complex arithmetic; division is not
3.9 Matrix transpose 87
exact, for example. Noting which properties are needed in a particular case is
our responsibility.
Exercise 3.15 Write functions to compute the binary sum and product of a list
of boolean values, using no built-in arithmetic.
Exercise 3.17 Using the results of the previous exercise, or by writing dummy
functions, extend structure Bin so that it matches signature ARITH.
Matrix transpose works well with this list representation because it goes sequen-
tially along rows and columns, with no jumping. The transpose function changes
the list of rows
[[x11 , x12 , . . . , x1m ],
.. .. ..
A = . . .
[xn 1 , xn 2 , . . . , xnm ]]
One way to transpose a matrix is by repeatedly taking columns from it. The
heads of the rows form the first column of the matrix:
88 3 Lists
fun headcol [] = []
| headcol ((x ::_) :: rows) = x :: headcol rows;
> ***Warning: Patterns not exhaustive
> val headcol = fn : ’a list list -> ’a list
Exercise 3.19 What input pattern do headcol and tailcols not handle? What
does transp return if the rows of the ‘matrix’ do not have the same length?
Exercise 3.20 What does transp do given the empty list? Explain.
3.10 Matrix multiplication 89
1 1 5 −1 2
The (1,1) element of the product above is computed by
(2, 0) · (1, 4) = 2 × 1 + 0 × 4 = 2.
In the dot product function, the two vectors must have the same length; ML
prints a warning that some cases are not covered. Henceforth these warnings
will usually be omitted.
fun dotprod ([], []) = 0.0
| dotprod (x ::xs,y::ys) = x *y + dotprod (xs,ys);
> ***Warning: Patterns not exhaustive
> val dotprod = fn : real list * real list -> real
If A has just one row, so does A × B . Function rowprod computes the product
of a row with B . The matrix B must be given as its transpose: a list of columns,
not a list of rows.
fun rowprod (row , []) = []
| rowprod (row , col ::cols) =
dotprod (row ,col ) :: rowprod (row ,cols);
> val rowprod = fn
> : real list * real list list -> real list
The matrix product function makes transp construct a list of the columns of B :
fun matprod (rowsA,rowsB ) = rowlistprod (rowsA, transp rowsB );
> val matprod = fn
> : real list list * real list list -> real list list
Here are the declarations of the sample matrices, omitting ML’s response:
val rowsA = [ [2.0, 0.0],
[3.0, ˜1.0],
[0.0, 1.0],
[1.0, 1.0] ]
and rowsB = [ [1.0, 0.0, 2.0],
[4.0, ˜1.0, 0.0] ];
Exercise
3.22 a −Ab matrix is negated by negating each of its components; thus
− ac db = − −c −d . Write a function to negate a matrix.
x + 2y + 7z = 7
−4w + 3y − 5z = −2 (∗)
4w − x − 2y − 3z = 9
−2w + x + 2y + 8z = 2
to a triangular form:
−4w + 3y − 5z = −2
x + 2y + 7z = 7
3y − z = 14
3z = 3
If the selected row has head p, then delrow (p, rows) removes it from the list of
rows.
fun delrow (p, []) = []
| delrow (p, row ::rows) = if p = hd row then rows
else row :: delrow (p, rows);
> val delrow = fn : ’’a * ’’a list list -> ’’a list list
Function elimcol , declared inside gausselim, refers to the current pivot row by
its head p (the leading coefficient) and tail prow . Given a list of rows, elimcol
replaces each by its sum with prow , properly scaled. The first element of each
sum is zero, but these zeros are never computed; the first column simply disap-
pears.
92 3 Lists
Function gausselim removes the pivot row, eliminates a column, and calls itself
recursively on the reduced matrix. It returns a list of pivot rows, decreasing in
length, forming an upper triangular matrix.
A system of n equations is solved by Gaussian elimination on an n × (n + 1)
matrix, where the extra column contains the right-side values. The solutions
are generated recursively from the triangular matrix. Known solutions are mul-
tiplied by their coefficients and added — this is a vector dot product — and
divided by the leading coefficient. To subtract the right-side value we employ a
trick: a spurious solution of −1.
fun solutions [] = [˜1.0]
| solutions((x ::xs)::rows) =
let val solns = solutions rows
in ˜(dotprod (solns,xs)/x ) :: solns end;
> val solutions = fn : real list list -> real list
Further reading. Researchers at the University of Wales have applied the lan-
guage Haskell to computational fluid dynamics problems. The aim is to in-
vestigate the practical utility of functional programming. One paper compares different
representations of matrices (Grant et al., 1996). Another considers the possibility of ex-
3.12 Writing a number as the sum of two squares 93
ploiting parallelism, using a simulated parallel processor (Grant et al., 1995). Compared
with conventional Fortran implementations, the Haskell ones are much slower and need
more space; the authors list some developments that could improve the efficiency.
Exercise 3.24 Show that if the input equations are linearly independent, then
division by zero cannot occur within gausselim.
Exercise 3.25 Do pivotrow and delrow work correctly if the heads of several
rows have the same absolute value?
Exercise 3.28 Write a structure Matrix that matches signature ARITH. You
can either use the previous exercise and those of Section 3.10, or write dummy
functions. This adds matrices to our collection of arithmetic structures.1
Dijkstra’s program has a different search method: x and y start with equal val-
ues, then sweep apart. Our method could well be the one he rejected because
‘the demonstration that no solutions had been omitted always required a draw-
ing.’
A smarter way? A number is the sum of two squares precisely if, in its prime
factorization, every factor of the form 4k + 3 appears with an even exponent.
For example, 48, 612, 265 = 5 × 13 × 17 × 29 × 37 × 41 and none of these primes
has the form 4k + 3. The criterion itself merely tells us whether solutions exist, but the
theory also provides a means of enumerating the solutions (Davenport, 1952, Chapter
V). A program exploiting this theory would pay off only for huge numbers.
3.13 The problem of the next permutation 95
3421
4231
2431
3241
2341
4312
3412
4132
1 Find the leftmost element y that has a greater element to its left. The
elements to its left will therefore be an increasing sequence x1 ≤ · · · ≤
xn . (We are really speaking of positions rather than elements, but this
only matters if the elements are not distinct.)
2 Replace y by the smallest xi , with 1 ≤ i ≤ n, such that y < xi ,
and arrange x1 , . . . , xi −1 , y, xi +1 , . . . , xn in descending order. This can
be accomplished by scanning xn , xn −1 , . . . , x1 until the correct value is
found for xi , placing larger elements in front of the final result.
96 3 Lists
Calling next(xlist, ys) finds the y in ys to replace, while xlist accumulates the
elements passed over. When xlist holds the reversed list [xn , . . . , x1 ], the func-
tion swap performs the replacement and rearrangement. The list manipulations
are delicate.
fun next(xlist, y::ys) : int list =
if hd xlist <= y then next(y::xlist, ys)
else (*swap y with greatest xk such that x>=xk>y*)
let fun swap [x ] = y::x ::ys
| swap (x ::xk ::xs) = (*x >= xk*)
if xk >y then x ::swap(xk ::xs)
else (y::xk ::xs)@(x ::ys)
(*x > y >= xk >= xs*)
in swap(xlist) end;
> val next = fn : int list * int list -> int list
Exercise 3.30 Does next still work if the ≤ comparison is replaced by < in
its second line? Justify your answer in terms of the two steps described above.
The type α = × (α = list) → bool means that mem may be applied to any list
whose elements permit equality testing.
Observe that setof may perform many equality tests. To minimize the use of
setof , the following functions can be applied to ‘sets’ — lists of distinct ele-
ments — to ensure that their result is a ‘set.’
Union. The list union(xs, ys) includes all elements of xs not already in ys,
which is assumed to consist of distinct elements:
fun union([],ys) = ys
| union(x ::xs, ys) = newmem(x , union(xs, ys));
> val union = fn : ’’a list * ’’a list -> ’’a list
The type variable ’’a indicates equality testing, here via newmem.
union([1,2,3], [0,2,4]);
> [1, 3, 0, 2, 4] : int list
Intersection. Similarly, inter (xs, ys) includes all elements of xs that also be-
long to ys:
3 Section 3.22 below considers more deeply the question of representing one
data structure using another.
3.15 Polymorphic set operations 99
The subset relation. Set T is a subset of S if all elements of T are also elements
of S :
infix subs;
fun ([] subs ys) = true
| ((x ::xs) subs ys) = (x mem ys) andalso (xs subs ys);
> val subs = fn : ’’a list * ’’a list -> bool
Recall that equality types may involve tuples, lists and so forth:
[("May",5), ("June",6)] subs [("July",7)];
> false : bool
Equality of sets. The built-in list equality test is not valid for sets. The lists
[3, 4] and [4, 3] are not equal, yet they denote the same set, {3, 4}. Set equality
ignores order. It can be defined in terms of subsets:
infix seq;
fun (xs seq ys) = (xs subs ys) andalso (ys subs xs);
> val seq = fn : ’’a list * ’’a list -> bool
[3,1,3,5,3,4] seq [1,3,4,5];
> true : bool
Sets ought to be declared as an abstract type, hiding the equality test on lists.
Powerset. The powerset of a set S is the set consisting of all the subsets of S ,
including the empty set and S itself. It can be computed by removing some
element x from S and recursively computing the powerset of S − {x }. If T is a
subset of S − {x } then both T and T ∪ {x } are subsets of S and elements of the
powerset. The argument base accumulates items (like x ) that must be included
in each element of the result. In the initial call, base should be empty.
fun powset ([], base) = [base]
| powset (x ::xs, base) =
powset(xs, base) @ powset(xs, x ::base);
> val powset = fn : ’a list * ’a list -> ’a list list
100 3 Lists
The ordinary type variables indicate that powset does not perform equality tests.
powset (rev ["the","weird","sisters"], []);
> [[], ["the"], ["weird"], ["the", "weird"], ["sisters"],
> ["the", "sisters"], ["weird", "sisters"],
> ["the", "weird", "sisters"]] : string list list
Using set notation, the result of powset can be described as follows, ignoring
the order of list elements:
powset(S , B ) = {T ∪ B | T ⊆ S }
Cartesian product. The Cartesian product of S and T is the set of all pairs
(x , y) with x ∈ S and y ∈ T . In set notation,
S × T = {(x , y) | x ∈ S , y ∈ T }.
Several functional languages support some set notation, following David Turner;
see Bird and Wadler (1988) for examples. Since ML does not, we must use
recursion over lists. The function to compute Cartesian products is surprisingly
complex.
fun cartprod ([], ys) = []
| cartprod (x ::xs, ys) =
let val xsprod = cartprod (xs,ys)
fun pairx [] = xsprod
| pairx (y::ytail ) = (x ,y) :: (pairx ytail )
in pairx ys end;
> val cartprod = fn : ’a list * ’b list -> (’a * ’b) list
Section 5.10 will demonstrate how higher-order functions can express this func-
tion. For now, let us continue with simple methods.
Exercise 3.32 How many equality tests does ML perform when evaluating the
following expressions?
1 mem upto(1,500)
setof (upto(1,500))
3.16 Association lists 101
Exercise 3.33 Compare union with the function itunion declared below. Which
function is more efficient?
fun itunion([],ys) = ys
| itunion(x ::xs, ys) = itunion(xs, newmem(x , ys));
Exercise 3.34 Write a function choose such that choose(k , xs) generates the
set of all k -element subsets of xs. For instance, choose(29, upto(1, 30)) should
return a list containing 30 subsets.
Exercise 3.35 The following function is simpler than cartprod . Is it better for
computing Cartesian products?
fun cprod ([], ys) = []
| cprod (x ::xs, ys) =
let fun pairx [] = cprod (xs,ys)
| pairx (y::ytail ) = (x ,y) :: (pairx ytail )
in pairx ys end;
A list of (key, value) pairs is called an association list. The function assoc finds
the value associated with a key by sequential search:
fun assoc ([], a) = []
| assoc ((x ,y)::pairs, a) = if a=x then [y]
else assoc(pairs, a);
> val assoc = fn : (’’a * ’b) list * ’’a -> ’b list
Its type, (α = ×β)list ×α = → β list, indicates that keys must have some equality
type α = , while values may have any type β at all. Calling assoc(pairs,x )
returns [] if the key x is not found, and returns [y] if y is found paired with x .
Returning a list of results is a simple method of distinguishing success from
failure.
assoc(battles, "Agincourt");
> [1415] : int list
assoc(battles, "Austerlitz");
102 3 Lists
Searching can be slow, but updating is trivial: put a new pair in front. Since
assoc returns the first value it finds, existing associations can be overridden.
Pairing names with types in a block-structured language is a typical application.
A name will be paired with several types in the association list if it is declared
in nested blocks.
Equality types: good or bad? Appel (1993) criticises ML’s equality polymor-
phism on several grounds. They complicate the language definition. They
complicate the implementation; data must have run-time tags to support equality test-
ing, or else an equality test must be passed implicitly to functions. Sometimes the
standard equality test is inappropriate, as in the case of a set of sets. The polymorphic
equality test can be slow.
Part of the justification for equality polymorphism is historical. ML is related to Lisp,
where functions like mem and assoc are among the most basic primitives. But even
Lisp has to provide different versions of these functions, performing different sorts of
equality tests. If ML did not have equality polymorphism, those functions could still be
expressed by taking the testing function as an extra argument.
Equality is really overloaded: its meaning depends upon its type. Other overloaded
functions are the arithmetic operators and functions to express values as strings. ML’s
treatment of overloading seems unsatisfactory, especially compared with Haskell’s ele-
gant type classes (Hudak et al., 1992). But type classes also complicate the language.
More seriously, a program cannot be executed — even in principle — without a full type
checking. Odersky et al. (1995) discuss an alternative setup; more research is needed.
This function differs from assoc by returning all values that are paired with a,
not just the first:
3.17 Graph algorithms 103
(a) a c f e g
(b) 1 6 4 3 5
nexts("e", graph1);
> ["f", "g"] : string list
(a) a c f e g
(b) a c 3 2 5
> ["a", "b", "e", "f", "g", "c", "d"] : string list
Adding an edge from f to d makes the graph cyclic. If that graph is searched
starting at b, one of the edges in the cycle is ignored. Also, part of the graph is
not accessible from b at all; see Figure 3.2.
depthf (["b"], ("f","d")::graph1, []);
> ["b", "e", "f", "d", "g"] : string list
After visiting a node x that has not been visited before, depth-first search recur-
sively visits each successor of x . In the list computed by nexts(x ,graph) @ xs,
the successors of x precede the other nodes xs that are awaiting visits. This list
behaves as a stack. Breadth-first search results if the list of nodes to visit be-
haves as a queue.
Depth-first search can also be coded as follows:
fun depth ([], graph, visited ) = rev visited
| depth (x ::xs, graph, visited ) =
depth (xs, graph,
if x mem visited then visited
else depth (nexts(x ,graph), graph, x ::visited ));
A nested recursive call visits the successors of x , then another call visits the
3.17 Graph algorithms 105
other nodes, xs. The functions depthf and depth are equivalent, although the
proof is subtle. By omitting a call to append (@), depth is a bit faster. More
importantly, since one call is devoted to visiting x , it is easily modified to detect
cycles in graphs and perform topological sorting.
go
dress wash up
shower eat
wake
Finding a linear sequence of events from such a graph is called topological sort-
ing. Sedgewick (1988) points out that depth-first search can do this if the visit to
node x is recorded after its successors have been searched. Thus x comes after
every node reachable from x : a topological sort in reverse.
This means a simple change to depth: put x on the result of the recursive
call instead of the argument. The list forms in reverse so no other reversal is
necessary.
fun topsort graph =
let fun sort ([], visited ) = visited
| sort (x ::xs, visited ) =
sort(xs, if x mem visited then visited
else x :: sort(nexts(x ,graph), visited ))
val (starts,_) = ListPair .unzip graph
in
sort(starts, [])
end;
> val topsort = fn : (’’a * ’’a) list -> ’’a list
The let declaration of sort allows this function to refer to graph. It also de-
106 3 Lists
clares starts, the list of all starting nodes of edges, to ensure that every node in
the graph is reached.
So how do we get to work?
topsort grwork ;
> ["wake", "eat", "washup", "shower", "dress", "go"]
> : string list
Reversing the list of edges gives a different answer for the graph:
topsort(rev grwork );
> ["wake", "shower", "dress", "eat", "washup", "go"]
> : string list
Looping is not acceptable; the function should somehow report that no solution
exists. Cycles can be detected by maintaining a list of all nodes being searched.
This list of nodes, called path, traces the edges from the start of the search.
fun pathsort graph =
let fun sort ([], path, visited ) = visited
| sort (x ::xs, path, visited ) =
if x mem path then hd [] (*abort!!*)
else sort(xs, path,
if x mem visited then visited else
x :: sort(nexts(x ,graph),x ::path,visited ))
val (starts,_) = ListPair .unzip graph
in sort(starts, [], []) end;
> val pathsort = fn : (’’a * ’’a) list -> ’’a list
An error message is better than looping, but pathsort aborts by making an er-
roneous function call (namely hd []), an ugly trick. The next chapter explains
how to declare an exception for such errors.
Exceptions are not the only way to report cycles. The following function
returns two results: a list of visits, as before, and a list of nodes found in cycles.
Maintaining two results, let us declare a function to add a visit:
3.17 Graph algorithms 107
These polymorphic graph functions are too slow for large graphs because of the
list searches. Restricting the nodes to integers, more efficient functions can be
written using the functional arrays of the next chapter.
Exercise 3.36 Modify pathsort to return [] if the graph has a cycle and the
singleton list [visited ] otherwise.
Exercise 3.37 Let (visited , cys) be the result of cyclesort. If the graph con-
tains many cycles, will cys contain a node belonging to each? What can be said
about visited if the graph contains cycles?
These algorithms usually sort an array. Apart from heap sort, where the array
encodes a binary tree, they are easily coded as functions on lists. Their time
complexity remains unchanged: not that a list sort will win a race against an
array sort! A complexity estimate such as ‘order n 2 time’ means the execution
time is proportional to n 2 . The list sort will have a higher constant of propor-
tionality.
This section compares several sorting functions, giving the time taken to sort
a list of 10,000 random numbers. These timings are informal but illustrate the
practical performance of each algorithm.
The Pascal version of quick sort by Sedgewick (1988) can sort the numbers in
110 msec. This roughly equals the best time for functional sorting. Pascal beats
ML if checking is disabled, but we give up the clarity and simplicity of functional
programming (never mind safety). The overheads of lists would matter less for
sorting, say, a bibliography, where the cost of comparisons would dominate.
How timings were measured. Timings were conducted on a Sun SuperSPARC
Model 61 computer running Standard ML of New Jersey, version 108. The
measurements were made using standard library facilities (structure Timer ), and in-
clude garbage collection time. Thanks to improvements in hardware and software, ML
programs run 20–40 times faster than they did in the first edition of this book.
The Pascal program was compiled using the Pascal 3.0 compiler. With array sub-
script checking disabled, the run-time drops to 75 msec. With full optimization the
program runs in just 34 msec, but afterwards prints a warning that it ‘may have pro-
duced nonstandard floating-point results.’ What risks are worth taking in our quest for
speed?
Calling nextrand with any seed between 1 and m − 1 yields another number in
this range, performing the integer calculation
(a × seed ) mod m.
3.19 Insertion sort 109
Real arithmetic is used to avoid integer overflow. The function works provided
mantissæ are accurate to 46 bits. When trying this on your machine, check that
the random numbers are exact integers.
Calling randlist(n,seed ,[]) generates a random list of length n starting
from seed . Because the list accumulates in tail , its order is reversed:
fun randlist (n,seed ,tail ) =
if n=0 then (seed ,tail )
else randlist(n-1, nextrand seed , seed ::tail );
> val randlist = fn
> : int * real * real list -> real * real list
The list of 10,000 random numbers is called rs. Here are the first 15.
val (seed ,rs) = randlist(10000, 1.0, []);
> val seed = 1043618065.0 : real
> val rs =
> [1484786315.0, 925166085.0, 1614852353.0, 721631166.0,
> 173942219.0, 1229443779.0, 789328014.0, 570809709.0,
> 1760109362.0, 270600523.0, 2108528931.0, 16480421.0,
> 519782231.0, 162430624.0, 372212905.0, ...] : real list
The type constraint real list resolves overloading of the comparison operator.
All the sorting functions have a type constraint.
We insert some numbers into [6.0], which is trivially sorted:
ins(4.0, [6.0]);
> [4.0, 6.0] : real list
ins(8.0,it);
> [4.0, 6.0, 8.0] : real list
ins(5.0,it);
> [4.0, 5.0, 6.0, 8.0] : real list
The append (@) can be eliminated by accumulating the sorted result in a second
argument. This version of quick sort, which is left as an exercise, takes only
about 110 msec.
Like its procedural counterpart, quick takes order n log n time in the average
case. If the input is already in increasing or decreasing order, then quick sort
takes order n 2 time.
3.21 Merge sort 111
Exercise 3.38 Express quick sort such that quicker (xs, sorted ) accumulates
the result in sorted , with no use of append.
Exercise 3.39 Write a function find such that find (xs, i ) returns the i th small-
est item in the list xs. This is called selection. Hoare’s algorithm for selection
is related to quick sort, and is much faster than sorting the list and returning the
i th element.
When sorting 10,000 items, the recursion in merge may be too deep for some
ML systems. The fault lies with those ML systems, not with merge. As with take
and append , the dominant cost is that of constructing the resulting list. An it-
erative merging function, although avoiding the deep recursion, would probably
have to perform costly list reversals.
Merge sort can be top-down or bottom-up. Either way, merging is efficient
only if the two lists have similar lengths. If a list has only one element, merging
degenerates to insertion.
Top-down merge sort. In the top-down approach, the input list is divided into
two roughly equal parts using take and drop. These are sorted recursively and
the results merged.
fun tmergesort [] = []
| tmergesort [x ] = [x ]
| tmergesort xs =
let val k = length xs div 2
in merge (tmergesort (List.take(xs,k )),
tmergesort (List.drop(xs,k )))
end;
> val tmergesort = fn : real list -> real list
112 3 Lists
Unlike quick sort, the worst case execution time is order n log n. But it is slower
on average, taking about 290 msec to sort the 10,000 numbers. Its calls to
length, take and drop scan the input list repeatedly. Here is one way to elimi-
nate them:
fun tmergesort 0 xs =
let fun sort (0, xs) = ([], xs)
| sort (1, x ::xs) = ([x ], xs)
| sort (n, xs) =
let val (l 1, xs1) = sort ((n+1) div 2, xs)
val (l 2, xs2) = sort (n div 2, xs1)
in (merge (l 1,l 2), xs2)
end
val (l , _) = sort (length xs, xs)
in l end;
Calling sort(n,xs) sorts the first n elements of xs and returns the remaining
elements. One might expect this to be slow, since it builds so many pairs. But
it needs only 200 msec to sort the random numbers. It is still slower than quick
sort, but can be recommended as a simple and acceptably fast method.
Bottom-up merge sort. The basic bottom-up approach divides the input into lists
of length 1. Adjacent pairs of lists are then merged, obtaining sorted lists of
length 2, then 4, then 8 and so on. Finally one sorted list remains. This approach
is easy to code but wasteful. Why should the 10,000 numbers be copied into a
list of 10,000 singleton lists?
O’Keefe (1982) describes a beautiful way to merge the lists of various lengths
simultaneously, never storing these lists in full.
AB C D E F G H I J K
The underlining shows how adjacent lists are merged. First A with B , then C
with D, and now AB and CD have equal length and can be merged. O’Keefe
accumulates the merges at all levels in one list. Rather than comparing the sizes
of lists, he lets the count k of members determine how to add the next member.
If k is even then there are two members of equal size s to merge. The resulting
list is treated as member k /2 of size 2s, which may cause further merging.
fun mergepairs([l ], k ) = [l ]
| mergepairs(l 1::l 2::ls, k ) =
if k mod 2 = 1 then l 1::l 2::ls
else mergepairs(merge(l 1,l 2)::ls, k div 2);
> val mergepairs = fn
> : real list list * int -> real list list
3.21 Merge sort 113
If k = 0 then mergepairs merges the entire list of lists into one list. Calling
sorting(xs,[[]],0) sorts the list xs. It takes 270 msec to sort the 10,000
numbers.
fun sorting([], ls, k ) = hd (mergepairs(ls,0))
| sorting(x ::xs, ls, k ) =
sorting(xs, mergepairs([x ]::ls, k +1), k +1);
> val sorting = fn
> : real list * real list list * int -> real list
A smooth sort has a linear execution time (order n) if its input is nearly sorted,
degenerating to n log n in the worst case. O’Keefe presents a ‘smooth applica-
tive merge sort’ that exploits order in the input. Rather than dividing it into
singleton lists, he divides the input into increasing runs. If the number of runs is
independent of n (and so ‘nearly sorted’) then the execution time is linear.
The function nextrun returns the next increasing run from a list, paired with
the list of unread items. (An imperative program would delete items as they
were processed.) The run grows in reverse order, hence the call to rev .
fun nextrun(run, []) = (rev run, []: real list)
| nextrun(run, x ::xs) =
if x < hd run then (rev run, x ::xs)
else nextrun(x ::run, xs);
> val nextrun = fn
> : real list * real list -> real list * real list
The algorithm is both elegant and efficient. Even for our random data, with its
short runs, the execution time is 250 msec.
Historical notes. Sorting with lists is similar to sorting on tape. Random ac-
cess is inefficient; data must be scanned in order, either forward or reverse.
The fascinating but largely obsolete literature on tape sorting may contain useful ideas.
114 3 Lists
The technique used in mergepairs bears a strong resemblance to the oscillating sort
developed during the 1960s (Knuth, 1973, §5.4.5).
Merge sorting is seldom done using arrays because it cannot be done in-place: it re-
quires two arrays. Even so, its use was proposed as early as 1945; the idea of exploiting
runs in the input is also quite old (Knuth, 1973, §5.2.4).
Exercise 3.41 Use the following function to code a new version of top-down
merge sort, and measure its speed. Explain your findings, taking account of
garbage collection time if you can measure it.
fun alts ([],xs,ys) = (xs,ys)
| alts ([x ],xs,ys) = (x ::xs, ys)
| alts (x ::y::l , xs, ys) = alts(l , x ::xs, y::ys);
Exercise 3.42 This is the same as the previous exercise, except that you should
base the new sorting function on the following:
fun takedrop ([], n, xs) = (xs, [])
| takedrop (x ::l , n, xs) =
if n>0 then takedrop(l , n-1, x ::xs)
else (xs, x ::l );
Exercise 3.44 Write a version of samsort that uses both increasing and de-
creasing runs.
Polynomial arithmetic
Computers were invented to perform numerical arithmetic. They excel
at portraying data graphically. But sometimes nothing conveys more informa-
tion than a symbolic formula. A graph of E = mc 2 is simply a straight line!
Computer algebra is concerned with automating symbolic mathematics, as
used by scientists and engineers. Systems such as MACSYMA and REDUCE can
do stupendous tasks involving differentiation, integration, power series expan-
sions, etc. The most basic symbolic operations perform polynomial arithmetic.
Even this is hard to do efficiently. We shall restrict ourselves to the simplest
case of all, univariate polynomials. These are polynomials in one variable,
say x ; they can be added and multiplied straightforwardly:
(x + 1) + (x 2 − 2) = x 2 + x − 1
(x + 1) × (x 2 − 2) = x 3 + x 2 − 2x − 2
3.22 Representing abstract data 115
by rational numbers, pairs of integers with no common factor.4 But this would
be a digression, and it really requires arbitrary-precision integers, which some
ML systems lack. Therefore, strictly as an expedient, let us represent coefficients
by the ML type real .
We could represent polynomials by the list of their coefficients, [an , . . . , a0 ].
To see that this is unacceptable, consider the polynomial x 100 + 1, and then
consider squaring it! In a typical polynomial, most of the coefficients are zero.
We need a sparse representation, in contrast to the dense representation that we
used earlier for matrices.
Let us represent a polynomial by a list of pairs of the form (k , ak ) for every k
such that ak is non-zero. The pairs should appear in decreasing order in k ; this
will help us collect terms having the same exponent. For example, [(2,1.0),
(0,˜2.0)] represents x 2 − 2.
Our representation is better behaved than that of finite sets because every
abstract polynomial has a unique representation. Two polynomials are equal
just if their underlying lists are equal. But not every list of (integer, real) pairs
represents a polynomial.
We ought to declare polynomials as an abstract type, hiding the underlying
lists. For now, let us package the polynomial operations into a structure, follow-
ing signature ARITH .
structure Poly =
struct
type t = (int *real ) list;
val zero = [];
fun sum ...
fun diff ...
fun prod ...
fun quo ...
end;
Here t is the type that represents polynomials, and zero is the empty list, which
represents the zero polynomial. The other components are described below.
The product of two polynomials is computed using the distributive law. Terms
of one polynomial are multiplied against the other polynomial, and the results
added:
Faster multiplication. Experiments show that nprod is too slow for large poly-
nomials. It requires over two seconds and numerous garbage collections to com-
pute the square of (x 3 + 1)400 . (Such large computations are typical of computer
algebra.) The reason is that sum merges lists of greatly differing length. If
ts and us have 100 terms each, then termprod ((m,a), us) has only 100
terms, while nprod (ts,us) could have as many as 10,000 terms. Their sum
will have at most 10,100 terms, a growth of only 1%.
Merge sort inspires a faster algorithm. Divide one of the polynomials into
equal halves, compute two products recursively, and add them. We form as
many sums as before, but they are balanced: we compute (p1 + p2 ) + (p3 + p4 )
instead of p1 + (p2 + (p3 + p4 )). On average the summands are smaller, and each
addition doubles the size of the result.
118 3 Lists
This is three times as fast as nprod for computing the square of (x 3 + 1)400 . The
speedup appears to increase with larger polynomials.
Running some examples. Although we have not fully defined the structure Poly
yet, suppose we have done so — including a function show , to display polyno-
mials as strings. We compute the sum and product of x + 1 and x 2 − 2, which
were shown at the start of this section:
val p1 = [(1,1.0),(0,1.0)]
and p2 = [(2,1.0),(0,˜2.0)];
Poly.show (Poly.sum (p1, p2));
> "xˆ2 + x - 1.0" : string
Poly.show (Poly.prod (p1, p2));
> "xˆ3 + xˆ2 - 2.0x - 2.0" : string
Exercise 3.45 Code diff , a function that computes the difference between two
polynomials. Using termprod it can be coded in one line, but not efficiently.
Exercise 3.46 Code show , the function that produces the output shown in the
text. (Functions Real .toString and Int.toString convert numbers to strings.)
Exercise 3.47 Give a convincing argument that sum and prod respect the rep-
resentation of polynomials.
3.25 The greatest common divisor 119
Exercise 3.48 What does it mean to say that sum correctly computes the sum
of two polynomials? How might you prove it?
2x +3
x− 1 )2x 2+ x −3
2
2x − 2x
3x −3
3x −3
0
Here the remainder is zero. In the general case, the remainder is a polynomial
whose leading exponent (called its degree) is less than that of the divisor. Let us
divide x 3 + 2x 2 − 3x + 1 by x 2 + x − 2, obtaining a remainder of −2x + 3:
x +1
2
x +x − 2 )x 3 + 2x 2
− 3x +1
x 3 + x 2 − 2x
x2 − x +1
x2 + x −2
−2x +3
If the divisor’s leading coefficient is not unity then fractions will probably ap-
pear. This can make the computation much slower, but it does not complicate the
basic algorithm. The function quorem directly implements the method sketched
above, returning the pair (quotient, remainder). The quotient forms in reverse.
120 3 Lists
We can trivially define the quotient function quo in terms of quorem. Includ-
ing the diff function of Exercise 3.45 gives structure Poly all the components
it needs to match signature ARITH . Our collection of arithmetic structures now
includes the complex numbers, binary numerals, matrices and polynomials! But
note, again, that there are important differences among these structures; for each
there are additional components that would not make sense for the others. Al-
though Poly can match signature ARITH when we need it to, normally we take
full advantage of its other components.
Euclid’s Algorithm for polynomials. We can now compute GCDs using Euclid’s
Algorithm. Recall that #2 extracts the second component of a pair, here the
remainder after polynomial division.
fun gcd ([], us) = us
| gcd (ts, us) = gcd (#2(quorem (us,ts)), ts);
Assuming that Poly contains gcd as a component, we can try it out on some
examples. The GCD of x 8 − 1 and x 3 − 1 is x − 1:
Poly.show (Poly.gcd ([(8,1.0),(0,˜1.0)],
[(3,1.0),(0,˜1.0)]));
> "x - 1.0" : string
3.25 The greatest common divisor 121