We use Haskell at PP in order to illustrate some functional programming concepts (or functional programming design patterns) which can be used in any functional or multi-paradigm lecture. These are:
With this in mind, we point out that this is not a course about the Haskell language - although we will quite a few of its particular aspects (with IO being a distinguished omission). In this page you will find the subset of programming constructs which are used during the lecture.
Functions are defined as follows:
<function name> <param1> ... <param n> = <body expression>
For instance:
f x y = x + y
Anonymous functions can be defined as follows:
\<param1> ... <param n> -> <body expression>
For instance:
\x y -> x + y
Function calls are of the form:
<function name> <v1> ... <vn>
for a function with at least n parameters.
For instance:
f 1 2
It is a good practice (although not always necessary) to enclose a function call in parentheses, e.g. (f 1 2)
instead of f 1 2
. This actually makes
the code more legible and avoids typing bugs.
To avoid cluttering of nested function calls, e.g. f (g (h x y))
, the function application operator $
can be used. The type of $
is (a→b)→a→b
, that is, it takes a function and an argument and applies the latter on the function. The previous call can be rewritten as:
f $ g $ h x y
You can also interpret $
as a means for enforcing precedence (i.e. first h
is called, then g
then f
).
Functional composition is often a good alternative to $
. For instance, the previous function call can be written as:
(f . g) (h x y)
Meaning that function (f . g)
is called with argument (h x y)
. Note that (f . g . h) x y
is not equivalent, and signifies the call of f . g . h
with arguments x
and y
.
Usually (although not mandatory), functions are defined in prefix form (as seen in the previous examples). Some Haskell functions are infix (e.g. +
, .
or $
). We can turn an infix function into a prefix one using parentheses. E.g. :t ($)
or (+) 1 2
. Similarly, we can turn an prefix function into an infix, using quasiquotes, e.g. 1 f 2
where f x y = x + y
.
Often we would like to define auxiliary functions whose scope (visibility) is limited to our function definition. We can do this using where
. We illustrate it on several examples:
f x y = (inc x) + y where inc v = v + 1
f x y = (inc x) + (dec y) where inc v = v + 1 dec v = v - 1
f x y = (inc x) + (dec y) where inc v = (g v) + 1 where g 0 = 0 g _ = 1 dec v = v - 1
f x y = (inc x) + (dec y) where inc v = (g v) + 1 g 0 = 0 g _ = 1 dec v = v - 1
The last two examples construct functions f
with the same behaviour, however they are not identical. In the latter example, the function g
is visible in the entire where
body, thus other functions defined in the same scope could employ it.
The basic datatypes used in the PP lecture are:
Integer
and Float
)Bool
)Char
)The container types are:
[a]
)(a,b)
)a → b
)
where a
and b
are type variables.
The base constructors for lists are:
[] :: [a]
(the empty list)(:) :: a → [a] → [a]
(cons)The list observers are:
head :: [a] → a
tail :: [a] → [a]
Other distinguished operators are:
(++) :: [a] → [a] → [a]
(appending two lists)The base constructor pairs is:
(,) :: a → b → (a,b)
Pair observers are:
fst :: (a,b) → a
snd :: (a,b) → b
The type String
in Haskell is an alias for [Char]
.
A powerful mechanism in Haskell is pattern matching, which allows defining function body-expressions based on how a value is constructed. In a pattern, only base constructors can be used. The simplest example is:
f [] = ... f (x:xs) = ...
which defines two body expressions, one for an argument equal to the empty list, and one for a list constructed via the cons operator. Here, x
is the first element of the list and xs
is the rest of the list.
Patterns can be combined liberally to express more complicated arguments such as:
(x:y:xs)
- a list of at least two elements(1:xs)
- a list which contains 1
as its first element[1,2]
- the list [1,2]
(1:2:[])
- same as above(x,y)
- a pair where x
is the first element and y
is the second( (x:xs),(y:ys) )
- a pair where both the first and the second elements are lists of at least one element_
- anything - or an anonymous value
Pattern matching in Haskell is not limited to function definitions, but can also be used in the body expression of a function, using case
expressions:
case <expression> of <pattern 1> -> <body expression 1> ... <pattern n> -> <body expression n>
The <expression>
is evaluated to a value, and the value is hence checked to see if it satisfies each pattern in turn. When a pattern is found, the case expression is evaluated to the pattern's respective body expression.
Example:
case foldr (:) [] [1,2,3] of [] -> 1 (x:xs) -> 2 (x:y:xs) -> 3 _ -> 4
In the previous example, foldr (:) [] [1,2,3]
evaluates to [1,2,3]
. The first pattern is not satisfied, however the second one is, thus the case expression will return 2
. The third pattern is more particular but occurs after the first satisfying pattern.
Instead of relying on how values are constructed, a programmer may want to define a function's body expression in terms of boolean conditions. This can be done using guards, as follows:
<function name> <param1> ... <param n> | <boolean condition 1> = <body expression 1> ... | <boolean condition n> = <body expression n> | otherwise = <body expression (n+1)>
Example:
f x y | x + 5 > y = 1 | x == y = 2 | x == head (sort [x,y]) = 3
In the previous example, f 1 5
will return 1
while f 4 5
will return 3
.