The functions we defined so far were restricted to elementary operations, such as incrementing a number. In this chapter, we will discuss some slightly more advanced functions and survey elementary operations on lists.
Usually, a program consists of a large number of function and type definitions. Obviously, putting them all into one single file is a bad idea. Modern programming languages therefore provide some means to structure the program by allowing to group related definitions into logical units which are stored in separate files. In Haskell, these units are called modules.
Let us have a look at the definition of a Haskell module, called Simple
:
-- Example module
-- Gabriele Keller, August 2015
--
-- This is a simple example of a module definition
module Simple
where
-- calculates the arithmetic mean of two numbers
--
arithmetic_mean :: Fractional a => a -> a -> a
arithmetic_mean x y = (x + y) / 2
-- calculates the harmonic mean of two numbers
--
harmonic_mean :: Fractional a => a -> a -> a
harmonic_mean x y = 2 * x * y / (x + y)
The module begins with a header: a comment containing a one line description of the module, the author and date of creation, and briefly describes the purpose of the module.1
The first line of code starts with the keyword module
, followed by the name of the module, the keyword where
, and the definitions that belong to the module. Note that a module name, in contrast to function or variable names must start with an uppercase letter.
In Haskell, there is a special module called Prelude
whose contents is always available. The module Prelude
contains all the functions that are pre-defined in Haskell, such as +
, length
, and so on.
For now, as we are starting with simple, short programs, we will add all function definitions of a program to a single module. Later we will learn how we can structure more complex programs using multiple modules. In fact, this is a central topic in software development, and modules play an essential role in structuring large software systems.
So far, each of our functions has unconditionally performed one specific computation. In general, this is too limiting; so, next, we will look at the role of conditionals in Haskell programs.
Choice by way of conditionals. How can we implement a function max
which returns the greater of its two arguments? That is, the expressions max 5 2
and max 2 5
both evaluate to 5
, max 1 7
to 7
, and so on. For two arbitrary numbers x
and y
, we want max x y
to return x
if x >= y
; otherwise, it should return y
.2
We can generally compare values with types that belong to the type class Ord
, which we discussed in First Steps; hence, the type of max
is
max :: Ord a => a -> a -> a
This can be expressed in Haskell by a so-called conditional or if-then-else expression of the form
if
⟨condition⟩ then
⟨value if true⟩ else
⟨value if false⟩
Now, we can implement max
:
max :: Ord a => a -> a -> a
max x y = if x >= y then x else y
Let’s look at the evaluation of max 5 2
:
max 5 2
⇒if 5 >= 2 then 5 else 2
⇒if True then 5 else 2
⇒5
Conditionals are an essential component of programming, because they allow us to dynamically choose between different computations depending on the values of the inputs. Here is another example:
signum :: (Ord a, Num a) => a -> Int
signum x = if x < 0 then -1 else if x == 0 then 0 else 1
A note about the type of signum
: the type of the argument x
has to be both in Ord
, as we use the function <
and ==
, but also in Num
, otherwise we would not be able to compare it to 0
, which is in type class Num
. We will cover type classes in more depth in a later chapter.
Guards. Cascading conditional expressions —as in the previous definition of signum
— are difficult to read; therefore, some programming languages, including Haskell, provide an alternative syntax:
signum :: (Ord a, Num a) => a -> Int
signum x | x < 0 = -1
| x == 0 = 0
| x > 0 = 1
The guards are checked in the order they are listed. For example, if we apply signum
to the number 7
, then the system first checks if the argument is less than zero. As this is not the case, it checks whether it is equal to zero, before finally the last guard succeeds:
signum 7
⇒1
on the basis of the following three side computations evaluating the guards
7 < 0
⇒False
7 == 0
⇒False
7 > 0
⇒True
Usually, the last guard should catch all the cases not covered before. For this purpose, we can use the special guard otherwise
, which always evaluates to True
:
signum :: (Ord a, Num a) => a -> Int
signum x | x < 0 = -1
| x == 0 = 0
| otherwise = 1
Finally, we can rephrase the definition of max
using guards:
max :: Ord a => a -> a -> a
max x y | x >= y = x
| otherwise = y
Which to choose is often a matter of personal style; however, idiomatic Haskell tends to favour code using guards.
The following screencast illustrates the use of conditionals and guards in Haskell for Mac.
A binder binds a value to a name. The value can subsequently be referred to by that name. For example, the Prelude
definition
pi :: Floating a => a
pi = 3.141592653589793
allows us to just write pi
instead of spelling out 3.141592653589793
. The type class Floating
contains the types Float
and Double
(i.e., single and double precision floating point numbers).
We may use a previously introduced name in another function definition:
circleArea :: Floating a => a -> a
circleArea radius = pi * radius * radius
Sometimes, we need to introduce a new name, which will only be used within a function. In that case, we should use a local binding. For example,
circleArea' :: Floating a => a -> a
circleArea' diameter = pi * radius * radius
where
radius = diameter / 2.0 -- local binding
The evaluation of this function proceeds as follows:
circleArea' 6.0
⇒pi * radius * radius where radius = 6.0 / 2.0
⇒
pi * radius * radius where radius = 3.0
⇒
pi * 3.0 * 3.0
⇒
pi * 9.0
⇒
3.141592653589793 * 9.0
⇒
28.2743
So far, we have seen how to pass multiple values to a function, but not how a function can return more than one result value. We can achieve this by using tuples:
addMul :: Num a => a -> a -> (a, a)
addMul x y = (x + y, x * y)
A tuple combines multiple components (two integer values, in the above example) into one compound value. The compound value can be manipulated as a single entity and, in particular, be returned as a value from a function.
However, the construction of a compound value is only half of the story. We also need a method for decomposing such values. We achieve this by using a notation dual to that of tuple construction:3
fst (x, y) = x
snd (x, y) = y
In the argument of fst
, we do not use a variable to refer to the compound argument as a whole. Instead, we decompose the pair into its components x
and y
— this is also called decomposition by pattern matching. The combined use of addMul
and fst
behaves as follows:
fst (addMul 5 6)
⇒fst (5 + 6, 5 * 6)
⇒
fst (11, 30)
⇒
11
The types of fst
and snd
. So far, we omitted the type annotations from the definitions of fst
and snd
. Generally, the Haskell system will automatically infer the types of definitions that lack a signature — however, it is good style to explicitly provide signatures. In all our previous definitions, the type was determined by the operations we applied to the arguments. For example max
was restricted to work on types which are members of the type class Ord
, since we needed the operator >=
, which can only compare certain types of values. The functions fst
and snd
are different, though. We don’t actually apply any function or operation to x
or y
, so they could be values of any type. In fact, x
and y
don’t even have to have the same type. Hence, we can just use type variables to represent the types of x
and y
without having to add any type class restrictions:
fst :: (a, b) -> a
fst (x, y) = x
snd :: (a, b) -> b
snd (x, y) = y
Functions like fst
and snd
, which can handle arguments of any type are called (parametric) polymorphic functions.
Example: points. Tuples are not just useful to return multiple results, but also for the representation of data items that cannot be modeled by one primitive value alone. A useful example is given by the points in a 2-dimensional Cartesian coordinate system: they can be represented by a pair of integer values. To avoid having to write the less informative (Int, Int)
whenever we denote the type of a point, we can introduce a new type name — similar to the introduction of names for repeatedly used values, which we discussed earlier:
type Point = (Int, Int)
With this definition, we define some simple operations on points:
-- origin of the coordinate system
--
origin :: Point
origin = (0, 0)
-- move a given point to the right
--
moveRight :: Point -> Int -> Point
moveRight (x, y) distance = (x + distance, y)
-- move a given point to upwards
--
moveUp :: Point -> Int -> Point
moveUp (x, y) distance = (x, y + distance)
Example: colour points. When we extend points to include a colour, another important property of tuples becomes obvious: tuple components may be of different types. Hence, if we denote colour values with a textual (string) representation, we have
-- we represent colours by strings
--
type Colour = String
-- new name for the type of colour points
--
type ColourPoint = (Int, Int, Colour)
which enables the following operations on colour points:
-- origin of the coordinate system in a given colour
--
origin :: Colour -> ColourPoint
origin colour = (0, 0, colour)
-- move a colour point vertically and horizontally
--
move :: ColourPoint -> Int -> Int -> ColourPoint
move (x, y, colour) xDistance yDistance
= (x + xDistance, y + yDistance, colour)
-- compute the distance between two colour points
--
distance :: ColourPoint -> ColourPoint -> Float
distance (x1, y1, colour1) (x2, y2, colour2)
= sqrt (fromIntegral (dx * dx + dy * dy))
where
dx = x2 - x1
dy = y2 - y1
Note how we use a where
clause in the last definition to avoid repeating the expressions x2 - x1
and y2 - y1
. The standard function fromIntegral
converts any integral type to any other numeric type. Its signature is
fromIntegral :: (Integral a, Num b) => a -> b
Important symmetries in Haskell. If we compare the syntax of values and types of tuples, we see that they correspond. For example, consider
(10, 15, "green") :: (Int, Int, String)
If we replace the values 10
, 15
, and "green"
with their respective types Int
, Int
, and String
, we obtain the type of the tuple. Moreover, we have a correspondence between term construction and term decomposition (also called pattern matching). Consider,
startPoint = (0, 0, "black")
colourOfPoint (x, y, colour) = colour
If we replace the components in the tuple construction (0
, 0
, and "black"
) by variable names (in this case x
, y
, colour
), we arrive at the pattern that can be used to decompose the corresponding tuple.
Special names for some tuples.
The following table lists a number of tuple types and their names:
# Expression Name 0 ()
Unit 1 n/a n/a 2 (x_1, x_2)
Pair 3 (x_1, x_2, x_3)
Triple 4 (x_1, x_2, x_3, x_4)
Quadruple 5 (x_1, x_2, x_3, x_4, x_5)
Quintuple ⋮ n (x_1, …, x_n)
n-tuple
Tuples provide the ability to bring together a fixed number of values of varying type. However, many applications, in addition, require the ability to manipulate compound types that may contain a varying number of elements of a single type. This is what lists are for.
We can create lists in a manner similar to pairs, by listing all components, separated by comma. The only difference is that we enclose these values in square brackets:
firstTenPrimes :: [Int]
firstTenPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 27]
Lists are a central data structure in Haskell, and the language offers a lot of convenient short cuts. We don’t need to explicitly write every single element if our list elements are just a sequence of consecutive numbers — or in fact, any type whose values can be enumerated:
oneToTwenty :: [Int]
oneToTwenty = [1..20]
where
oneToTwenty
⇒[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
If the elements follow a simple pattern of the form [n, n + k, n + 2*k, n + 3*k ....,m]
we simply write the first two numbers and the last one, and the compiler will figure out what the elements are:
-- return all positive odd numbers up to maxNumber
oddNumbers :: Int -> [Int]
oddNumbers maxNumber = [1, 3..maxNumber]
The length of the list returned by this function depends on the argument, unlike with the earlier addMul
, where only the value, but not the number of values, depended on the input. For example, we have
oddNumbers 10
⇒[1, 3, 5, 7, 9]
oddNumbers 15
⇒[1, 3, 5, 7, 9, 11, 13, 15]
The difference between tuples and lists can be seen by comparing their types, as in
(1, 2, "green") :: (Int, Int, String)
and
[1, 2, 3, 4] :: [Int]
The number of components is explicit in the type of a tuple, but not in the type of a list. As a consequence, the elements of tuples may be heterogeneous, whereas those of lists must be homogeneous.
Adding elements to the front of a list. The right-associative operator (:)
adds another element to the front of an existing list:
(:) :: a -> [a] -> [a]
For example, we have
"blue" : []
⇒["blue"]
"yellow" : ["red", "green", "blue"]
⇒
["yellow", "red", "green", "blue"]
"yellow" : "red" : "green" :"blue" : []
⇒
["yellow", "red", "green", "blue"]
The operator (pronounced cons) can only add elements at the front. So,
["red", "green", "blue"] : "yellow"
⇒ Error!
The cons-operator is another example of a polymorphic function, as it works on lists of any type. The only restriction is that the element we are adding is of the same type as the elements of the list we pass as the second argument:
(:) :: a -> [a] -> [a]
Joining two lists. We can join two lists together using the operator
(++) :: [a] -> [a] -> [a]
Just as the cons operator, it works on lists of any type, as long as the two lists have the same type of elements:
[4, 2, 3] ++ [3, 1, 2, 7]
⇒[4, 2, 3, 3, 1, 2, 7]
[True] ++ [False, True]
⇒[True, False, True]
You may remember this operator from the previous chapter, where we used it to join two strings to define
exclaim :: String -> String
exclaim sentence = sentence ++ "!"
This works, because strings are simply lists of characters in Haskell.
Extract the element at a specific index position out of a list. We can index into a list and extract an element using
(!!) :: [a] -> Int -> a
On lists of ints
[0, 1, 2, 3] !! 2
⇒2
or strings (lists of characters)
"Hello" !! 4
⇒'o'
As you can see, the index count starts at 0. If we apply the index operation to an index which is out of range for the given list, evaluation will raise an exception and we get an error message.
Split a list into its first element and the rest.
head :: [a] -> a
tail :: [a] -> [a]
Both functions require the input list to be non-empty, and if they are applied to an empty list, we get a runtime error.
head [0, 1, 2, 3]
⇒0
tail [0, 1, 2, 3]
⇒[1, 2, 3]
head "mouse"
⇒'m'
tail "mouse"
⇒"ouse"
Length of a list.
length :: [a] -> Int
For example, we have
length [0, 1, 2, 3]
⇒4
Check if an item is contained in a list. We can check if an item is element of a given list if its type is in the class Eq
. This restriction is necessary because the implementation of this function needs to compare the items for equality.
elem :: Eq a => a -> [a] -> Bool
For example, we have
elem 2 [0, 1, 2, 3]
⇒True
elem 5 [1, 2, 3, 4]
⇒False
elem 'o' "Hello"
⇒True
Add up or multiply the elements of a list. We can calculate the maximum, minumum, sum, or the product of all the elements of a list with these two functions.
maximum :: Ord a => [a] -> a
minimum :: Ord a => [a] -> a
sum :: Num a => [a] -> a
product :: Num a => [a] -> a
For example, we get
sum [0, 1, 2, 3]
⇒0 + 1 + 2 + 3
⇒6
product [1, 2, 3, 4]
⇒1 * 2 * 3 * 4
⇒24
Given these functions, how can we add "yellow"
at the end of ["red", "green", "blue"]
? We can’t use :
(cons), since it can only add elements at the start of a list. We need to use ++
as follows:
["red", "green", "blue"] ++ ["yellow"]
Note the list constructing brackets around "yellow:"
. In effect, we wrap "yellow"
into a singleton list, which we then append to the list ["red", "green", "blue"]
.
The following screencast illustrates list processing by way of a few examples, each of which highlights some of the functions that we just discussed.
As lists and tuples are often confused, let us summarise the differences between them. Tuples have the following properties:
Fixed size, i.e., fixed number of components: the pair (1, 2) :: (Int, Int)
and the triple (1, 2, 3) :: (Int, Int, Int)
have different types.
Components may be of different type: (5, Hello)
makes perfect sense.
In contrast, the following are the properties of lists:
Variable size, i.e., number of components may vary: [1, 2] :: [Int]
and [1, 2, 3] :: [Int]
have the same types.
Components must be of the same type: [5, Hello]
is incorrect.
Strings are in fact a particular form of lists in Haskell, which are defined as follows in the Prelude
:
type String = [Char]
Hence, list operations work on strings. "Hello" !! 1
⇒ 'e'
In fact, in Haskell, "Hello"
is exactly the same as ['H', 'e', 'l', 'l', 'o']
. This is very convenient, as we will see that there are many powerful list processing operations and these are directly available for string manipulation.
Hence, if we ask the Haskell system for the type of an expression that produces a list, it may report the type as either String
or as [Char]
. As these two types have been deemed equal by the above type definition, they may be used interchangeably
Some list functions, such as length
, may be applied to any list and always produce a meaningful result. In contrast, some other functions, such as head
and tail
, fail for some lists. For example, if we apply head
to an empty list, we get a runtime error or runtime exception.
head []
⇒ ** Exception: Prelude.head: empty list
The function head
is defined by pattern matching using the same symmetry between list construction and list pattern matching as we discussed previously for tuples — i.e., it matches on the cons-operator (:)
:
head :: [a] -> a
head (x:xs) = x
When head
gets applied to the empty list, there is simply no function equation that matches this case. We call such functions partial functions. Their definition ignores —or is invalid for— some of the input values admitted by the function's type signature. We discussed that types represent sets of related values; hence, partial functions are functions that are only defined for a subset of the values included in their argument types. The opposite are total functions. Total functions, such as length
, are defined for all values included in the sets represented by their argument types.
Partial functions are a common source of programming mistakes. Hence, good style dictates that the documentation of a partial function (e.g., as a comment preceding the function definition) includes a statement specifying the range of admissible input values (and specifies what happens if the function is incorrectly called with an argument that is outside of this range).
The Haskell Prelude
includes the function
error :: String -> a
that always results in a runtime error using its argument as the error message — that is, we have
error "encountered a fatal error"
⇒ ** Exception: encountered a fatal error
We can use error
to customise the error message of a partial function. For example, the complete definition of head
in the Haskell Prelude
reads as follows:
head :: [a] -> a
head (x:_) = x
head [] = error "Prelude.head: empty list"
This is better than the default error message generated by the Haskell compiler, which includes the location of the failing function definition, but doesn't specify the argument value that led to the failure.
Note the use of the underscore _
as the second argument of the cons-operator in the first equation. It represents a pattern matching position whose value is not used in the body of the function definition. It is better style than using a regular variable name, such as xs
, as it immediately signals that the corresponding value goes unused. Instead of a plain underscore, we can also use variable names starting with an underscore character (here, for example _xs
) to indicate that the value is not used.
Unlike in many other programming languages, formatting matters in Haskell. In other words, the correct use of indentation and newlines is crucial to avoid errors. This allows the language to do away with some of the noise that is introduced by some other languages to disambiguate the input — in particular, Haskell programs don’t need curly braces or semicolon to delimit blocks and statements.
Compare
foo x
= a + b
where
a = 1 + x
b = 2
to
foo x
= a + b
where
a = 1 + x
b = 2
Both are legal programs. However, in the first one, the definition of b
is part of the where binding
and therefore local to foo
whereas in the second program, the use of b
is not restricted to foo
.
An example of proper layout is the function distance
that we discussed earlier:
distance :: ColourPoint -> ColourPoint -> Float
distance (x1, y1, colour1) (x2, y2, colour2)
= sqrt (fromIntegral (dx * dx + dy * dy))
where
dx = x2 - x1
dy = y2 - y1
There are three layout rules that we have to follow to get syntactically correct programs:
All program code that belongs to a function definition has to be further to the right than the first character of that definition (i.e., the first character of the function name). In the case of distance
, all code has to be further to the right than the column in which the character d
of the function name distance
is located.
Similarly, all code of a local definition in a where
clause must be further to the right than the first character of the name of the variable defined by the binding.
All definitions within a where
clause must be aligned — e.g., above the definitions of dx
and dy
start in the same column.
Alternatively, we can explicitly group the bindings of a where
clause using curly braces, and separate the individual bindings by semicolons:
distance (x1, y1, colour1) (x2, y2, colour2)
= sqrt (fromIntegral (dx * dx + dy * dy))
where {dx = x2 - x1; dy = y2 - y1}
Inside the curly braces, we can format the code anyway we like, because the compiler can still figure out what belongs where. Nevertheless, while the following program is legal
distance (x1, y1, colour1) (x2, y2, colour2)
= sqrt (fromIntegral (dx * dx + dy * dy)) where {dx
= x2 -
x1; dy = y2 - y1}
it is hard to read and clearly pretty bad style. Seasoned Haskell programmers who prefer explicit bracketing align the braces and the semicolons at the beginning of each line (which looks rather strange to, say, a C-programmer, who is used to having semicolons at the end of the line):
distance (x1, y1, colour1) (x2, y2, colour2)
= sqrt (fromIntegral (dx * dx + dy * dy))
where
{ dx = x2 - x1
; dy = y2 - y1
}
The following screencast illustrates proper layout and the ways in which layout can go wrong.
Write a function sort2 :: Ord a => a -> a -> (a, a)
which accepts two Int
values as arguments and returns them as a sorted pair, so that sort2 5 3
is equal to (3,5)
. How can you define the function using a conditional, how can you do it using guards?
Consider a function
almostEqual :: Eq a => (a, a) -> (a, a) -> Bool
which compares the values of two pairs. It returns True
if both pairs contain the same values, regardless of the order. For example, almostEqual (3,4) (4,3)
is True
, but almostEqual (3,4) (3,5)
is False
. Which of the following definitions return the correct value? Which of the definitions would you consider good style? Why?
(The operator (&&)
is logical ”and”, the operator (||)
is logical ’or’, and (==)
tests if two values are equal. The first two are of type Bool -> Bool -> Bool
; the third is of type Eq a => a -> a -> Bool
.)
almostEqual (x1, y1) (x2, y2)
| (x1 == x2) && (y1 == y2) = True
| (x1 == y2) && (y1 == x2) = True
| otherwise = False
almostEqual (x1, y1) (x2, y2)
| (x1 == x2) = (y1 == y2)
| (x1 == y2) = (y1 == x2)
| otherwise = False
almostEqual pair1 pair2
= (pair1 == pair2) || (swap pair1 == pair2)
where
swap (x,y) = (y,x)
almostEqual pair1 pair2
= (pair1 == pair2) || (swap pair1 == swap pair2)
where
swap (x,y) = (y,x)
almostEqual (x1, y1) (x2, y2)
= if (x1 == x2)
then
if (y1 == y2)
then True
else False
else
if (x1 == y2)
then
if (x2 == y1)
then True
else False
else False
Define a function isLower :: Char -> Bool
which returns True
if a given character is a lower case letter. You can use the fact that characters are ordered, and for all lower case letters ch we have ′a′ ≤ ch and ch ≤ ′z′. Alternatively, you can use the fact that ['a'..'z']
evaluates to a list containing all lower case letters.
Write a function mangle :: String -> String
which removes the first letter of a word and attaches it at the end. If the string is empty, mangle
should simply return an empty string:
mangle "Hello"
⇒"elloH"
mangle "I"
⇒"I"
mangle ""
⇒""
Implement division on Int
, divide :: Int -> Int -> Int
using the list functions described in this section. Hint: first, write a function that returns all the multiples of a given number up to a specific limit.
divide 5 10
⇒2
divide 5 8
⇒1
divide 3 10
⇒3
The header is optional in that the compiler will not raise an error if it is missing.↩
The function max
, as well as signum
discussed later, are already defined in the Prelude
; so, if we want to define these functions ourselves, we need to add the line import Prelude hiding (max, signum)
right after the module header to hide the standard definitions.↩
Both fst
and snd
are already defined in the Prelude
.↩