Skip to main content

Introduction

Goals

  • Learn how to define functions
  • How types help you while programming
  • Syntax of Haskell
  • How to define and use datatypes
  • Overview of base types and datatypes

Structure of a Haskell program

  • Haskell programs comprise one or more modules. One module per file. Main module is always called Main.
  • Modules consist of declarations. Declarations introduce datatypes, functions and constants, type classes and instances.
  • We will focus on functions and constants first, then datatypes. Later type classes and instances.

Declaring new functions and constants

length :: [a] -> Int
length [] = 0
length (x : xs) = 1 + length xs
  • The name being introduced.
  • Type signature (optional, but recommended).
  • One or more equations defining the function.
  • The = symbol separates the left hand sides from the right hand sides.
  • Cases are distinguished by patterns.
  • On the right hand side, we have expressions.

Declarations, patterns, expressions

Informally:

  • A (function or constant) declaration binds a (new) identifier to an expression.
  • A pattern occurs as an argument to an identifier on the left hand side of a declaration. It introduces names that are available on the right hand side. Patterns can be matched against actual function arguments. Matches can fail or succeed.
  • Expressions occur on the right hand side of a function definition. Expressions can be evaluated.

Types

Every expression must have a type in Haskell – otherwise it will be rejected by the compiler:

  • Haskell types can be inferred. There’s usually no need for type annotations.
  • Use :t in GHCi to obtain the inferred type of an expression.
  • Type annotations (using :: ) are optional. But if they’re given, their correctness is checked.

How to define a function?

There are two main design principles for defining functions:

  • by (systematic) pattern matching and recursion,
  • by applying a higher-order function (such as composition, map , foldr , ...) and thereby reducing the problem to smaller subproblems.

In both cases, thinking about the types first helps you! We will focus on the pattern matching approach first.

Functions on lists

Most functions operate on structured data. Lists are a simple data structure, so they’re ideal for learning.

Recall from the Quick Tour

Lists are defined inductively:

  • The empty list [] is a list.
  • Given a single element y and a list ys , we can construct a new list y : ys (pronounced y cons ys ).

We call [] and (:) the constructors of the list datatype.

Another look at elem

Let’s try to implement elem once more, systematically.

elem :: Int -> [Int] -> Bool

We start with the type.

Do we want to restrict ourselves to Int lists? No!

elem :: a -> [a] -> Bool

Let’s make as few assumptions as possible.

In order to split up the programming problem, let’s take a look at the input list ...

elem :: a -> [a] -> Bool
elem x [] = ...
elem x (y : ys) = ...

There are two cases, one per constructor of the list datatype. Let’s see if we can solve the simple case for [] .

elem :: a -> [a] -> Bool
elem x [] = False
elem x (y : ys) = ...

Now to the cons-case. The ys is a shorter list – the most natural way to define functions on recursive datatypes is to use recursive functions.

Let’s try to implement elem once more, systematically.

elem :: a -> [a] -> Bool
elem x [] = False
elem x (y : ys) = ... elem ys ...

Let’s try to complete the second case making use of the recursive call.

elem :: a -> [a] -> Bool
elem x [] = False
elem x (y : ys) = x == y || elem x ys

Done?

elem :: a -> [a] -> Bool
elem x [] = False
elem x (y : ys) = x == y || elem x ys

Oh, we actually need equality on the elements. That seems to be a sensible requirement for elem , so let’s refine the type ...

elem :: Eq a => a -> [a] -> Bool
elem x [] = False
elem x (y : ys) = x == y || elem x ys

Now we’re really done

elem :: Eq a => a -> [a] -> Bool
elem x [] = False
elem x (y : ys) = x == y || elem x ys

The systematic development we’ve just seen generalizes to most functions on lists and most functions on other structured datatypes.

Mapping over a list

map :: (a -> b) -> [a] -> [b]

Start with the type. A function is like any other argument.

map :: (a -> b) -> [a] -> [b]
map f [] = ...
map f (x : xs) = ...

Introduce cases based on the list constructors.

map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x : xs) = ...

Solve the simple case first.

map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x : xs) = ... map f xs ...

Keep recursion in mind.

map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x : xs) = f x : map f xs

As f is a function, we can apply it. Take a final look. Everything looks fine.

drop elements from a list

The call drop n xs should remove the first n elements from xs .

drop :: Int -> [a] -> [a]
drop n [] = ...
drop n (x : xs) = ...

What do we actually want to do if we want to drop 3 elements of an empty list?

drop :: Int -> [a] -> [a]
drop n [] = []
drop n (x : xs) = ...

We take a simple approach.

drop :: Int -> [a] -> [a]
drop n [] = []
drop n (x : xs) = ... drop ... xs ...

Wait, but what we want to do depends on n ? We have multiple options here ...

drop :: Int -> [a] -> [a]
drop n [] = []
drop 0 (x : xs) = ... drop ... xs ...
drop n (x : xs) = ... drop ... xs ...

We can pattern match on an Int too ... If cases overlap, the first matching case applies.

drop :: Int -> [a] -> [a]
drop n [] = []
drop 0 (x : xs) = x : xs
drop n (x : xs) = ... drop ... xs ...

Sometimes, we don’t need to recurse – even though we could.

drop :: Int -> [a] -> [a]
drop n [] = []
drop 0 (x : xs) = []
drop n (x : xs) = drop (n - 1) xs

Done. But what happens with negative numbers as arguments?

drop :: Int -> [a] -> [a]
drop n [] = []
drop n (x : xs) =
if n <= 0
then x : xs
else drop (n - 1) xs

We can use if - then - else . We can include negative numbers now. If an equation spans multiple lines, the subsequent lines must be indented with respect to the first.

drop :: Int -> [a] -> [a]
drop n [] = []
drop n (x : xs)
| n <= 0 = x : xs
| otherwise = drop (n - 1) xs

Yet another option: use so-called guards. Can only appear directly after the pattern match. Boolean conditions are tried in order, otherwise is just a constant that is defined to be True .

Exercise – define the following functions

Append two lists: (++) :: [a] -> [a] -> [a]

Hint: Only pattern match on the first list (i.e., don’t distinguish more cases than needed).

Reverse a list: reverse :: [a] -> [a]

Hint: Follow the standard pattern, and make use of (++) that you have just defined.

Excursion: infix operators

Haskell allows you to create your own operators from a given set of symbols:

  • names are either completely symbolic or completely alphanumeric;
  • symbolic names are by default used infix, but can be used in prefix notation by surrounding them in parentheses (Example: (+) 2 3 evaluates to 5 );
  • alphanumeric names are by default used prefix, but can be used in infix notation by surrounding them in backquotes (Example: 8 mod 3 evaluates to 2 );
  • you can define the associativity and priority of infix operators by using infix , infixl , and infixr declarations;
  • by using :i or :info in GHCi, you can obtain information about the priority of infix operators.

Filtering a list

We want to traverse a list and keep all elements that have a certain property. Question How to best express a property of an element? As a function from the element to a Bool . Recall from the Quick Tour: a Bool is a another datatype with two constructors, called True and False .

Defining filter

filter :: (a -> Bool) -> [a] -> [a]

We can now write down the type

filter :: (a -> Bool) -> [a] -> [a]
filter p [] = ...
filter p (x : xs) = ... filter ... xs ...
filter :: (a -> Bool) -> [a] -> [a]
filter p [] = []
filter p (x : xs) = ... filter ... xs ...

It depends on the outcome of p x what we want to do! We have several options here.

filter :: (a -> Bool) -> [a] -> [a]
filter p [] = []
filter p (x : xs) = if p x
then x : filter p xs
else filter p xs

We can use the built-in if - then - else construct. Note that Bool is a type like any other. No need to write p x == True . Plain p x is simpler and equivalent.

filter :: (a -> Bool) -> [a] -> [a]
filter p [] = []
filter p (x : xs)
| p x = x : filter p xs
| otherwise = filter p xs

We can also use guards – each guard is tried in order. Note that

otherwise :: Bool
otherwise = True

Using filter

There are some useful predicates:

even, odd :: Integral a => (a -> Bool)
isUpper, isDigit :: Char -> Bool

We can also define our own:

positiveInt :: Int -> Bool
positiveInt n = n > 0

palindrome :: [Char] -> Bool
palindrome xs = reverse xs == xs

Note that String is a (type) synonym for [Char]

Try using filter with these predicates.

Excursion: anonymous functions

In practice, functions such as filter are often used with lambda expressions or anonymous functions:

filter (\ n -> n > 10 && even n) [1 . . 100]

A lambda expression is a way to define a function without giving it a name:

myPredicate n = n > 10 && even n

is just different syntax for

myPredicate = \ n -> n > 10 && even n

Excursion: operator sections

Partially applied infix operators have yet again special syntax:

\ n -> n > 10

can be abbreviated to

(> 10)

Similarly, we can write (1 +) or ("Hello " ++) or (`div`5).

So it’s possible to say

filter (> 10) [1 . . 100]