This definition starts with a specification of the
type of the append function: the way I have written
append here
takes in a pair of lists of some type
a (a variable type)
and produces a single list of the same type. The next two
lines specify the function: this is done by considering the two
possibilities for the first argument and describing what happens in
each case. This is often called a definition by structural
recursion as we break down
the structure of the first argument. When the first argument is
the empty list the function returns the second argument, while if
the first argument is a non-empty list we must recurse on the tail of
the
list in the expected manner.
The system evaluates this function by trying each pattern in turn until
it finds the first one which matches. It then applies the
transformation suggested by the RHS of that pattern. This does
mean that the
order in which patterns are written is important.
Functional programs are all about using (defined) functions within
other functions. Here is a little example: to reverse a
list we can use the
append function above:
reverse:: [a] -> [a]
reverse [] = []
reverse (x:xs) = append(reverse xs,[x])
This is an O(n^2) reverse (naive reverse) but it does express clearly
the intention of reversing a list! Here is an O(n) reverse:
rev:: [a] -> [a]
rev xs = rev1 (xs, [])
where rev1 ([], ys) = ys
rev1 (x:xs, ys)
= rev1 (xs,x:ys)
This code is marginally less clear: note the use of a local definition
to introduce the second argument.
More
on reverse!
Curried programs:
I have written all the above programs in uncurried form: thus, if a
function needs two arguments I have passed them
in as a tuple. However, functional languages are better suited
to defining functions in a curried form. This means that one
would tend to define
append as follows:
append:: [a] -> [a] -> [a]
append [] ys = ys
append (x:xs) ys = x:(append xs ys)
where the type
[a] -> [a] -> [a] (which
should be read as
[a] -> ([a] -> [a]) -- the
association is to the right) says the function takes in an element of
[a]
and produces a function
[a] -> [a]. This means we
should read
append x y as
(append x) y with association
to the left! Intitially these conventions may seem a little
confusing but they are conventions which are well-established and they
do, overall, reduce the need for parentheses. However, this does
mean that when you intend to first apply
f to
x then
g
to the result you must use parentheses (see also the composition
notation) to get the desired effect:
g (f x) not g f x = (g f) x.
Using the curried form does mean that the function with just one
argument applied is a valid term:
append [1,2,3]:: [Integer] -> [Integer]
notice here the type is specialized as the first argument determines
that the general type must be an integer.
Here is a program listing for the programs
above and some others I discussed in class.
Datatypes:
One of the most convenient features of a functional programming
language is its datatype definitions. These allow one to declare
complex datatypes efficiently and to use them in a very convenient way.
We have already used the list datatype so let us illustrate how
one can write an insertion into a binary search tree. First it is
necessary to define the datatype of binary search trees:
data Ord a => STree a
= Tip
| Snode (STree a,a,STree a)
Here we have actually also required that the elements in the binary
search tree must admit an order.
Tip and
Snode
are called constructors. Their types are as follows:
tip:: () -> STree(a)
Snode:: (STree a,a,STree a) -> STree a
This allows us to define an insertion as follows:
insert:: Ord a => a -> (STree a) -> (STree a)
insert a Tip = Snode(Tip,a,Tip)
insert a (Snode(t1,b,t2))
| a < b
= Snode(insert a t1, b, t2)
| a =
b = Snode(t1,b,t2)
| a > b
= Snode(t1,b,insert a t2)