Introduction to Programming, Aug-Dec 2006
Lecture 2, Thursday 10 Aug 2006
Multiple definitions
Haskell does not limit us to a single definition for a function.
We can give multiple definitions which are scanned from top to
bottom. The first definition that matches is used to compute the
value of the output. For instance, here is an alternative
definition of xor.
xor :: Bool -> Bool -> Bool
xor True False = True
xor False True = True
xor b1 b2 = False
When does a function invocation match a definition? We have to
check that it matches for each argument. If the definition has a
variable for an argument, then any value supplied when invoking
the function matches on that argument and the value supplied is
uniformly substituted for the variable throughout the definition.
On the other hand, if the definition has a constant value for an
argument, the value supplied when invoking the function must
match precisely.
For instance, in the revised definition of xor, if we invoke the
function as xor False True, the first definition does not
match, but the second one does. If we invoke the function as
xor True True, the first two definitions both fail to match and
we end up using the third one.
We can use multiple definitions to define a function inductively.
For instance, here is a definition of the function factorial.
factorial :: Int -> Int -> Int
factorial 0 = 1
factorial n = n*(factorial (n-1))
If we write, for instance, factorial 3, then only the second
definition matches, leaving us with the expression 3*(factorial
2), after uniformly substituting 3 for n and
simplifying (3-1) to 2. We use the second definition
two more times to get 3*(2*(factorial 1)) and then
3*(2*(1*(factorial 0))). Now, the first definition matches,
and we get 3*(2*(1*(1))) which Haskell can evaluate using its
built-in rules for * to return 6.
Notice that there is no guarantee that an inductive definition in
Haskell is correct, nor that it terminates on all inputs.
Reflect, for instance, on what would happen if we invoked our
function as factorial (-1).
Observe the bracketing in the second defintion above. We write
n*(factorial (n-1)). This says we should compute (n-1),
then feed this to factorial and multiply the result by
n. If, instead, we write n*(factorial n-1), Haskell
would interpret this as n*((factorial n)-1)-in other words,
feed n to factorial, subtract 1 from the result and then
multiply by n. For arithmetic and relational expressions, the
normal precedence rules of arithmetic apply, so an unbracketed
expression such as x <= 5 || y > 6 would be implicitly
bracketed correctly as (x <= 5) || (y > 6). However, function
application binds more tightly than arithmetic operators, so
factorial n-1 is interpreted as (factorial n)-1 rather than
factorial (n-1).
Function definitions with guards
Often, a function definition applies only if certain conditions
are satisfied by the values of the inputs. Here is an example of
how to define factorial to work with negative inputs. If the
input is negative, we negate it and invoke factorial on the
corresponding positive quantity.
factorial :: Int -> Int
factorial 0 = 1
factorial n
| n < 0 = factorial (-n)
| n > 0 = n * (factorial (n-1))
In this version of factorial , the second definition has two
options depending on the value of n. If n < 0, the
first definition applies. If n > 0, the second definition
applies. These conditions are called guards, since they
restrict entry to the definition that follows. Each guarded
definition is signalled using
|. Notice that lines beginning with | are indented.
This tells Haskell that these lines are continuations of the current
definition.
Observe that we can combine definitions of different types. In
this example, the first definition, factorial 0 is a simple
expression while the second defintion is a conditional one.
The guards in a conditional definition are scanned from top to
bottom. They may overlap, in which case the definition that is
used is the one corresponding to the first guard that is
satisfied. For instance, we could write:
factorial :: Int -> Int
factorial 0 = 1
factorial n
| n < 0 = factorial (-n)
| n > 1 = n * (factorial (n-1))
| n > 0 = n * (factorial (n-1))
Now, factorial 2 would match the guard n > 1 while
factorial 1 would match the guard n > 0.
The guards in a conditional defintion may also not cover all
cases. For instance, suppose we write:
factorial :: Int -> Int
factorial 0 = 1
factorial n
| n < 0 = factorial (-n)
| n > 1 = n * (factorial (n-1))
factorial 1 = 1
Now, the invocation factorial 1 matches neither guard and falls
through (fortunately) to the third definition. If we had not supplied
the third definition, any invocation other than factorial 0
would eventually have tried to evaluate factorial 1, for which
no match would have been found, leading to the Haskell interpreter
printing an error message like the following:
Program error: pattern match failure: factorial 1
Often, we do want to catch all leftover cases in the last guard.
Rather than tediously specify the options that have been left
out, we can use the word otherwise, as in the following
definition of xor:
xor :: Bool -> Bool -> Bool
xor b1 b2
| b1 && not(b2) = True
| not(b1) && b2 = True
| otherwise = False
In this definition, note that since b1 and b2 are of type Bool,
we can directly write b1 && not(b2) instead of the more
explicit version b1 == True && b2 == False.
More on pattern matching
When we match a function invocation with a defintion involving
variables, the variables are uniformly substituted by the values
supplied. However, each input variable in the function
definition would be distinct. Consider the following function,
which checks if both its inputs are equal:
isequal :: Int -> Int -> Bool
isequal x y = (x == y)
It is tempting to try and rewrite this function as follows:
isequal :: Int -> Int -> Bool
isequal x x = True
isequal x y = False
The idea would be that the first definition implicitly checks whether
both arguments are equal by forcing them to both match x and
hence match each other. However, this is illegal in Haskell: each
variable on the left hand side of a definition should be distinct.
Sometimes, an argument is not used on the right hand side of a
definition. Consider the following definition that computes
x^n.
power :: Float -> Int -> Float
power x 0 = 1.0
power x n | n > 0 = x * (power x (n-1)
Here, the value of x^0 is 1.0 for all values of
x. In such a situation, we can use a special variable _
that matches any argument but cannot be used on the right hand side of
a definition.
power :: Float -> Int -> Float
power _ 0 = 1.0
power x n | n > 0 = x * (power x (n-1)
Unlike normal variables, we can use more than one copy of _ in
a definition, since the corresponding value cannot be used on the
righthand side in any case. As an example, here is a function that
checks if at least two of its three Bool arguments are
True.
twoofthree :: Bool -> Bool -> Bool -> Bool
twoofthree True True _ = True
twoofthree True _ True = True
twoofthree _ True True = True
twoofthree _ _ _ = False
How Haskell "computes"
Computation in Haskell is like simplifying expressions in
algebra. Relatively early in school, we learn that (a+b)2 is
a2 + 2ab + b2. This means that wherever we see (x+y)2 in an
expression, we can replace it by x2 + 2xy + y2.
In the same way, Haskell computes by rewriting expressions using
functions and operators. We say rewriting rather than
simpliyfing because it is not clear, sometimes, that the
rewritten expression is "simpler" than the original one!
To begin with, Haskell has rewriting rules for operations on
built-in types. For instance, the fact that 6+2 is 8 is embedded
in a Haskell rewriting rule that says that 6+2 can be rewritten
as 8. In the same way, True && False can be rewritten
to False, etc.
In addition to the builtin rules, the function definitions that
we supply are also used for rewriting. For instance, given the
following definition of factorial
factorial :: Int -> Int -> Int
factorial 0 = 1
factorial n = n*(factorial (n-1))
here is how "factorial 3" would be evaluated. In the following,
we use → to denote rewrite to:
| factorial 3
| → | 3 * (factorial (3-1)) |
| → | 3 * (factorial (2)) |
| → | 3 * (2 * factorial (2-1)) |
| → | 3 * (2 * factorial (1)) |
| → | 3 * (2 * (1 * factorial (1-1))) |
| → | 3 * (2 * (1 * factorial (0))) |
| → | 3 * (2 * (1 * 1)) |
| → | 3 * (2 * 1) |
| → | 3 * 2 |
| → | 6 |
When rewriting expressions, brackets may be opened up to change
the order of evaluation. Sometimes, more than one rewriting
path may be available. For instance, we could have completed the
computation above as follows.
| factorial 3
| → | 3 * (factorial (3-1)) |
| → | 3 * (factorial (2)) |
| → | 3 * (2 * factorial (2-1)) |
| → | (3 * 2) * (factorial (2-1)) <== New expression |
| → | 6 * (factorial (2-1))) |
| → | 6 * (factorial (1)) |
| → | 6 * (1 * factorial (1-1)) |
| → | 6 * (1 * factorial (0)) |
| → | 6 * (1 * 1) |
| → | 6 * 1 |
| → | 6 |
In Haskell, the "result" of a computation is an expression that
cannot be further simplified. In general, it is guaranteed that
any path we follow leads to the same "result", if a "result" is
found. It could be that one choice of simplification could yield
a result while another may not. For instance, using our
definition of power
power :: Float -> Int -> Float
power _ 0 = 1.0
power x n | n > 0 = x * (power x (n-1)
we could consider the expression power (8.0/0.0) 0.
Using the first rule, this reduces as
power (8.0/0.0) 0 → 1.0
However, if we first try to simplify (8.0/0.0), we get an
expression without a value so, in a sense, we have
power (8.0/0.0) 0 → Error
Alternatively, we could even try to evaluate an expression such as
power (1.0 * factorial (-1)) 0
where the first rule for power yields the result 1.0
while repeatedly trying to simplify the argument (1.0 *
factorial (-1)) will go into an unending sequence of
simplifications yielding no result.
Haskell uses a form of simplification that is called lazy-it
does not simplify the argument to a function until the value of the
argument is actually needed in the evaluation of the function. In
particular, Haskell would evaluate both the expressions above to
1.0. We will examine the consequences of having such a lazy
evaluation strategy at a later stage in the course.
Lists
Suppose we want a function that finds the maximum of all values from a
collection. We cannot use an individual variable to represent each
value in the collection because when we write our function definition
we have to fix the number of variables we use, which limits our
function to work only with collections that have exactly that many
variables.
Instead, we need a way to collectively associate a group of values
with a variable. In Haskell, the most basic way of collecting a group
of values is to form a list. A list is a sequence of values of a
fixed type and is written within square brackets separated by commas.
Thus, [1,2,3,1] is a list of Int, while
[True,False,True] is a list of Bool. The underlying
type of a list must be uniform: we cannot write lists such as
[1,2,True] or [3.0,'a']. A list of underlying type
T has type [T]. Thus, [1,2,3,1] is of type
[Int], [True,False,True] is of type [Bool],
...
Lists can be nested: we can have lists of lists. For instance,
[[1,2],[3],[4,4]] is a list each of whose members is a list of
Int, so the type of this list is [[Int]].
The empty list is uniformly denoted [] for all list types.
Internal representation of lists
Internally, Haskell builds lists incrementally, one element at a time,
starting with the empty list. This incremental building can be done
from left to right (each new element is tagged on at the end of the
current list) or from right to left (each new element is tagged on at
the beginning of the current list). For historical reasons, Haskell
chooses the latter, so all lists are built up right to left, starting
with the empty list.
The basic listbuilding operator, denoted :, takes an element
and a list and returns a new list. For instance 1:[2,3,4]
returns [1,2,3,4]. As mentioned earlier, all lists in Haskell
are built up right to left, starting with the empty list. So,
internally the list [1,2,3,4] is actually
1:(2:(3:(4:[]))). We always bracket the binary operator
: from right to left, so we can unambiguously leave out the
brackets and write [1,2,3,4] as 1:2:3:4:[]. It is
important to note that all the human readable forms of a list
[x1,x2,x3,...,xn] are internally represented canonically as
x1:x2:x3:...:xn:[]. Thus, there is no difference between the
lists [1,2,3], 1:[2,3], 1:2:[3] and
1:2:3:[].
Defining functions on lists
Most functions on lists are defined by induction on the structure of
the list. The base case specifies a value for the empty list. The
inductive case specifies a way to combine the leftmost element with an
inductive evaluation of the function on the rest of the list. The
functions head and tail return the first element and the
rest of the list for all nonempty lists. These functions are
undefined for the empty list. We can use head and tail
in our inductive definitions.
Here is a function that computes the length of a list of Int.
length :: [Int] -> Int
length [] = 0
length l = 1 + (length (tail l))
Notice that if the second definition matches, we know that l is
nonempty, so tail l retuns a valid value.
In general, the inductive step in a list based computation will
use both the head and the tail of the list to build up the final
value. Here is a function that computes the sum of the elements
of a list of Int.
sum :: [Int] -> Int
sum [] = 0
sum l = (head l) + (sum (tail l))
File translated from
TEX
by
TTH,
version 3.74.
On 15 Aug 2006, 10:50.