More List Processing Functions
Recursion often yields simple and natural definitions of functions on lists.
The following function computes the length of its argument list by distinguishing between the empty list (the basis case) and non-empty lists (the general case).
- fun length(L) =
= if (L=nil) then 0
= else 1+length(tl(L));
val length = fn : ''a list -> int
- length[1,2,3];
val it = 3 : int
- length[[5],[4],[3],[2,1]];
val it = 4 : int
- length[];
val it = 0 : int
The next function has a similar recursive structure. It doubles all the elements in its argument list (of integers).
- fun doubleall(L) =
= if L=[] then []
= else (2*hd(L))::doubleall(tl(L));
val doubleall = fn : int list -> int list
- doubleall[1,3,5,7];
val it = [2,6,10,14] : int list
The Reverse of a List
Concatenation of lists, for which we gave a recursive definition in the last lecture, is actually a built-in operator in ML, denoted by the symbol @.
We use this operator in the following recursive definition of a function that produces the reverse of a list.
- fun reverse(L) =
= if L = nil then nil
= else reverse(tl(L)) @ [hd(L)];
val reverse = fn : ''a list -> ''a list
- reverse [1,2,3];
val it = [3,2,1] : int list
- reverse nil;
val it = [] : ''a list
Pattern Matching
We have informally used pattern matching in different context, e.g., when applying inference rules or logical equivalences.
Informally, a pattern is an expression containing variables, for which other expressions may be substituted. The problem of matching a pattern against a given expression consists of finding a suitable substitution that makes the pattern identical to the desired expression, if one exists at all.
For example, we may apply De Morgan's Law,
to the formula
to obtain an equivalent formula
Here the ``meta-variables'' and were replaced by the formulas and q, respectively, making the left-hand side of De Morgan's law above identical to the subformula
of the given formula.
Function Definition by Patterns
In ML there is an alternative form of defining functions via patterns.
The general form of such definitions is:
fun <identifier>(<pattern1>) = <expression1>where the identifiers, which name the function, are all the same, all patterns are of the same type, and all expressions are of the same type.
| \ <identifier>(<pattern2>) = <expression2>
| \ ...
| \ <identifier>(<patternK>) = <expressionK>;
For example, an alternative definition of the reverse function is:
- fun reverse(nil) = nil
= | reverse(x::xs) = reverse(xs) @ [x];
val reverse = fn : 'a list -> 'a list
In applying such a function to specific arguments, the patterns are inspected in order and the first match determines the value of the function.
- reverse nil;
val it = [] : 'a list
- reverse[3,4,5];
val it = [5,4,3] : int list
Removing Elements from Lists
The following function removes all occurrences of its first argument from its second argument list.
- fun remove(x,L) =
= if (L=[]) then []
= else (if (x=hd(L))
= then remove(x,tl(L))
= else hd(L)::remove(x,tl(L)));
val remove = fn : ''a * ''a list -> ''a list
- remove(1,[5,3,1]);
val it = [5,3] : int list
- remove(2,[4,2,4,2,4,2,2]);
val it = [4,4,4] : int list
- remove(2,nil);\ val it = [] : int list
We use it as an auxiliary function in the definition of another function that removes all duplicate occurrences of elements from its argument list.
- fun removedupl(L) =
= if (L=[]) then []
= else hd(L)::remove(hd(L),removedupl(tl(L)));
val removedupl = fn : ''a list -> ''a list
- removedupl[1,2,3];
val it = [1,2,3] : int list
- removedupl[2,2];
val it = [2] : int list
- removedupl[2,4,2,4,4,2,4,2];
val it = [2,4] : int list
Constructing Sublists
A sublist of a list L is any list obtained by deleting some (i.e., zero or more) elements from L.
For example, [], [1], [2], and [1,2] are all the sublists of [1,2].
Let us design an SML function that constructs all sublists of a given list L. The definition will be recursive, based on a case distinction as to whether L is the empty list or not.
If L is non-empty, it has a first element x. There are two kinds of sublists: those containing x, and those not containing x.
For instance, in the above example we have sublists [1] and [1,2] on the one hand, and [] and [2] on the other hand.
Note that there is a one-to-one correspondence between the two kinds of sublists, and that each sublist of the latter kind is also a sublist of tl(L).
Constructing Sublists (continued)
These observations lead to the following definition.
- fun sublists(L) =Here @ denotes the (built-in) concatenation operation on lists, and the function insertL inserts its first argument at the front of all elements in its second argument (which must be a list). Its definition is left as an exercise.
= if (L=[]) then [nil]
= else sublists(tl(L))
= @ insertL(hd(L),sublists(tl(L)));
val sublists = fn : ''a list -> ''a list list
- sublists[];
val it = [[]] : ''a list list
- sublists[1,2];
val it = [[],[2],[1],[1,2]] : int list list
- sublists[1,2,3];
val it = [[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]] : int list list
- sublists[4,3,2,1];
val it = [[],[1],[2],[2,1],[3],[3,1],[3,2],
. [3,2,1],[4],[4,1],...
If we change the expression in the else-branch to
= else insertL(hd(L),sublists(tl(L)))all sublists will still be generated, but in a different order.
= @ sublists(tl(L))
Greatest Common Divisor
The calculation of the greatest common divisor (gcd) of two non-negative integers can also be done recursively based on the following observations:
A possible definition in ML is as follows:
- fun gcd(m,n):int = if m=n then n
= else if m>n then gcd(m-n,n)
= else gcd(m,n-m);
val gcd = fn : int * int -> int
- gcd(12,30);
val it = 6 : int
- gcd(1,20);
val it = 1 : int
- gcd(126,2357);
val it = 1 : int
- gcd(125,56345);
val it = 5 : int