Crash course into Haskell's syntax

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:

  • programming with side-effect-free (or pure) functions
  • programming with higher-order functions
  • programming with Abstract (or Algebraic) Datatypes
  • programming with lazy evaluation

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 

Parentheses and $

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.

Infix and prefix

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.

where

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:

  • integers and floats (Integer and Float)
  • booleans (Bool)
  • char (Char)

The container types are:

  • lists ([a])
  • pairs ((a,b))
  • functions (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.