Introduction to Functional Programming

Russ Ross

Computer Laboratory
University of Cambridge
Lent Term 2005


Lecture 3

Lists

Immutable, singly linked lists are part of the core language. Elements in a given list must be of a single type.

The prepend operator :: is called cons and it is right-associative. Writing a full list

[elt1, elt2, …, eltn]

is just shorthand for

elt1 :: elt2 :: … :: eltn :: []

Creating lists

The type of a list includes the type of the elements it contains

- [1,5,9];
> val it = [1, 5, 9] : int list

- [[], [3], [7,8]];
> val it = [[], [3], [7, 8]] : int list list

- "three" :: ["blind", "mice"];
> val it = ["three", "blind", "mice"] : string list

- [(1, true), (2, false), (3, true)];
> val it = [(1, true), (2, false), (3, true)] : (int * bool) list

Lists are immutable and can only be modified by prepending new elements

- (0, false) :: it;
> val it = [(0, false), (1, true), (2, false), (3, true)] : (int * bool) list

Pattern matching

Data constructors are also used to deconstruct data. A pattern match occurs when an expression is given that could have created the data being matched, given the right bindings to the names that appear.

Names that appear in the pattern are bound to the corresponding parts of the data object. Constants require an exact match, and _ matches anything without binding it to a name.

- fun null [] = true
    | null (x::xs) = false;
> val 'a null = fn : 'a list -> bool

- fun fact 1 = 1
    | fact n = n * fact (n - 1);
> val fact = fn : int -> int

Patterns can be used in function definitions (fun and fn), val bindings, and explicit case ... of ... end expressions.

Pattern examples

Value bindings are always done through pattern matches. Any data constructors—built-in or user defined— can appear in a pattern.

- val a :: [b, c] = [5, 6, 7];
> val a = 5 : int
  val b = 6 : int
  val c = 7 : int

- val x :: y :: z = [1,2,3,4,5];
> val x = 1 : int
  val y = 2 : int
  val z = [3, 4, 5] : int list

- fun first (a, _) = a;
> val ('a, 'b) first = fn : 'a * 'b -> 'a

- fun getfirst x = first x;
> val ('a, 'b) getfirst = fn : 'a * 'b -> 'a

Pattern matching is the only way to access the constituents of a data structure.

Deep patterns

Patterns can be as deep or as shallow as required. They can match a value or the components that make up that value.

- val x = [(1, ([(2, "hi", true), (3, "yo", false)]))];
> val x = [(1, [(2, "hi", true), (3, "yo", false)])] :
  (int * (int * string * bool) list) list

- val y :: z = x;
> val y = (1, [(2, "hi", true), (3, "yo", false)]) :
  int * (int * string * bool) list
  val z = [] : (int * (int * string * bool) list) list

- val (a, ((b, c, d) :: e)) :: [] = x;
> val a = 1 : int
  val b = 2 : int
  val c = "hi" : string
  val d = true : bool
  val e = [(3, "yo", false)] : (int * string * bool) list

Only one pattern can be used with val, but fun, fn, and case expressions can include multiple patterns. They are tried in order until one one matches the shape of the object being matched.

Standard list functions

To compute the length of a list, count its elements recursively

- fun length [] = 0
    | length (_::xs) = 1 + length xs;
> val 'a length = fn : 'a list -> int

To get the head of a list, pattern match for it

- fun hd (x::_) = x;                     
> Toplevel input:
! fun hd (x::_) = x;
!     ^^^^^^^^^^^^^
! Warning: pattern matching is not exhaustive

> val 'a hd = fn : 'a list -> 'a

Similar for tl, the tail function.

Changing lists

Lists are immutable; to make changes you have to copy up to the point you make the change.

- fun append [] ys = ys
    | append (x::xs) ys = x :: (append xs ys);
> val 'a append = fn : 'a list -> 'a list -> 'a list

Append is the infix operator @. We can use it for a simple (but awful) list reversal function

- fun badreverse [] = []                      
    | badreverse (x::xs) = badreverse xs @ [x];
> val 'a badreverse = fn : 'a list -> 'a list

badreverse copies half the list on average in each step.

badreverse [1,2,3,4,5] => badreverse [2,3,4,5] @ [1]
badreverse   [2,3,4,5] => badreverse   [3,4,5] @ [2]
badreverse     [3,4,5] => badreverse     [4,5] @ [3]
badreverse       [4,5] => badreverse       [5] @ [4]
badreverse         [5] => badreverse        [] @ [5]

Local bindings

Another way to write the length function uses an accumulator to gather the running total

- fun length2 [] a = a
    | length2 (_::xs) a = length2 xs (a + 1);
> val 'a length2 = fn : 'a list -> int -> int

This requires an extra argument. A local function defined and used inside another function lets us present the desired function type to the outside world while still passing an extra parameter in the main recursive loop.

Use let … in … end to define local functions and other values.

- fun length lst =
    let fun f [] a = a
          | f (_::xs) a = f xs (a + 1)
    in f lst 0
    end;
> val 'a length = fn : 'a list -> int

Tail recursion

The better way to reverse a list uses the same idea; it requires an extra argument in the main recursive loop

- fun rev lst = 
    let fun f [] a = a
          | f (x::xs) a = f xs (x::a)
    in f lst []
    end;
> val 'a rev = fn : 'a list -> 'a list

This version is sometimes called iterative, or tail recursive

Tail recursion

Not all functions can be made tail recursive. To be tail recursive, a function must do all of its work before passing all necessary data as arguments to the next iteration.

In general, operations that are commutative can be implemented with or without using tail recursion. If the order matters, then you are usually forced one way or the other.

When manipulating lists, tail recursive functions tend to reverse the list. You have to work from the head of a list, not the tail, and you can only prepend elements to a new list, not append them.

As a result, tail recursive functions that copy a list have the side-effect of reversing it. The last element from the source list becomes the first element of the new list. Compare these two copy functions

- fun copy1 (x::xs) = x :: (copy1 xs)
    | copy1 []      = [];
> val 'a copy1 = fn : 'a list -> 'a list

- fun copy2 lst =
    let fun f (x::xs) a = f xs (x::a)
          | f [] a = a
    in f lst []
    end;
> val 'a copy2 = fn : 'a list -> 'a list

More list functions

We can turn a pair of lists into a list of pairs

- fun zip (x::xs, y::ys) = (x,y) :: zip (xs, ys)
    | zip _              = [];                
> val ('a, 'b) zip = fn : 'a list * 'b list -> ('a * 'b) list

Note that _ (or a name) can match a compound value as well as simple values. Pattern matches are evaluated in the order they are written, so the second pattern acts as a fallthrough that catches anything the first pattern misses.

unzip reverses the actions of zip

- fun unzip ((x,y) :: tail) = let val (xs, ys) = unzip tail
                              in (x::xs, y::ys)
                              end
    | unzip [] = ([], []);
> val ('a, 'b) unzip = fn : ('a * 'b) list -> 'a list * 'b list

More list functions

Using name as pattern we can name an item and its subcomponents at the same time

- fun insertinorder n (lst as x::xs) =
          if n <= x then n :: lst
          else x :: (insertinorder n xs)
    | insertinorder n _ = [n];
> val insertinorder = fn : int -> int list -> int list

Note that insertinorder copies the list until it finds the insertion point, then it points to the remainder of the existing list.

Lists are immutable, so the remainder of the old list is just as good as a new copy. It can't change.