Introduction to Functional Programming

Russ Ross

Computer Laboratory
University of Cambridge
Lent Term 2005


Lecture 6

Record types

Records with named fields can be defined as a series of name=value pairs

- val war = { country="Iraq", wmd=false, oil=true };
> val war = {country = "Iraq", oil = true, wmd = false} :
  {country : string, oil : bool, wmd : bool}

Tuples are just records with numbered field names

- val tup = { 1="hi", 3=16, 2=true };
> val tup = ("hi", true, 16) : string * bool * int

Fields can be accessed using #field record

- #wmd war;
> val it = false : bool

- #2 tup;
> val it = true : bool

Records and pattern matching

Fields can also be given names using pattern matching

- val { country=target, oil=attack, wmd=_ } = war;
> val target = "Iraq" : string
  val attack = true : bool

It is okay to select a subset of fields

- val { country=target, oil=attack, ...} = war;
> val target = "Iraq" : string
  val attack = true : bool

As a shortcut, you can use the field name as the value name

- val { country, oil, ... } = war;
> val country = "Iraq" : string
  val oil = true : bool

Named types

A type can be given a name

- type employee = { name: string, salary: int, age: int}; 
> type employee = {age : int, name : string, salary : int}

- fun tax (e: employee) =
      real (#salary e) * 0.22;
> val tax = fn : {age : int, name : string, salary : int} -> real

- fun tax ({salary,...}: employee) =
      real (salary) * 0.22;
> val tax = fn : {age : int, name : string, salary : int} -> real

Enumerated types

Consider the King and his court:

datatype degree = Duke
                | Marquis
                | Earl
                | Viscount
                | Baron

datatype person = King
                | Peer of degree * string * int
                | Knight of string
                | Peasant of string

This defines a series of constructors for each type. To create an instance of the type, use the constructor

- val me = Peasant "Russ";
> val me = Peasant "Russ" : person

Constructors must be unique.

Using enumerated types

All the variants are part of a single type

- [King,
   Peer (Duke, "Gloucester", 5),
   Knight "Gawain",
   Peasant "Jack Cade"];
> val it = … : person list

Pattern matching uses the same constructors

- fun superior (King, Peer _) = true
    | superior (King, Knight _) = true
    | superior (King, Peasant _) = true 
    | superior (Peer _, Knight _) = true
    | superior (Peer _, Peasant _) = true
    | superior (Knight _, Peasant _) = true 
    | superior _ = false;
> val superior = fn : person * person -> bool

Exceptions

Exceptions are raised on various runtime failures including failed pattern match, overflow, out-of-memory error, etc. You can also define custom exceptions and raise them explicitly.

- exception Failure;
> exn Failure = Failure : exn

- exception Bad of int;
> exn Bad = fn : int -> exn

- raise Failure;
> Uncaught exception: 
! Failure

- raise (Bad 10);
> Uncaught exception: 
! Bad

Exception handlers take the form

E handle P1 => E1 | … | Pn => En

Recursive datatypes

Datatype definitions can be recursive, including polymorphic datatypes. We can define a binary search tree as

datatype 'a tree = Lf | Br of 'a * 'a tree * 'a tree;
…
- Br("little", Br("three", Lf, Lf), Br("pigs", Lf, Lf));
> val it = Br("little", Br("three", Lf, Lf), Br("pigs", Lf, Lf)) :
string tree

The standard list is no different from a regular datatype

infixr 5 ::
datatype 'a list = nil | op:: of 'a * 'a list;

Binary search trees

Binary search trees are a simple way to represent sets. When balanced, they offer O(n log n) runtime for all basic operations. A binary search tree is a binary tree where data elements are held in the interior nodes (not in the leaves), and the element in a node is greater than all elements in the left subtree and less than all elements in the right subtree.

We can test membership in a set using lookup

- fun lookup Lf (_:string) = false
    | lookup (Br (elt, left, right)) x =
        if x < elt then lookup left x
        else if x > elt then lookup right x
        else true;
> val lookup = fn : string tree -> string -> bool

To insert a new value, we search for the proper place, insert the new value, and copy every branch on the path to the new value

- fun insert Lf (x:string) = Br(x, Lf, Lf) 
    | insert (Br (elt, left, right)) x =
        if x < elt then Br (elt, insert left x, right)
        else if x > elt then Br (elt, left, insert right x)
        else Br (elt, left, right);
> val insert = fn : string tree -> string -> string tree

Using exceptions

To avoid copying anything when as existing element is inserted, we can use exceptions

- exception Duplicate;
> exn Duplicate = Duplicate : exn

- fun insert Lf (x:string) = Br(x, Lf, Lf)
    | insert (tree as Br (elt, left, right)) x =
        (if x < elt then Br (elt, insert left x, right)
        else if x > elt then Br (elt, left, insert right x)
        else raise Duplicate)
      handle Duplicate => tree;
> val insert = fn : string tree -> string -> string tree

This version raises an exception and catches it at every level of the tree. It would be better to use only one exception to go all the way back to the top level

- fun insert tree (x:string) =
    let fun ins Lf x = Br(x, Lf, Lf)
          | ins (Br (elt, left, right)) x =
              if x < elt then Br (elt, ins left x, right)
              else if x > elt then Br (elt, left, ins right x)
              else raise Duplicate
    in ins tree x end
    handle Duplicate => tree;
> val insert = fn : string tree -> string -> string tree

Functions on trees

We can write recursive functions for trees

- fun count Lf = 0
    | count (Br (_, l, r)) = 1 + count l + count r;
> val 'a count = fn : 'a tree -> int

- fun depth Lf = 0
    | depth (Br (_, l, r)) = 1 + Int.max(depth l, depth r);
> val 'a depth = fn : 'a tree -> int

- fun inorder Lf = []
    | inorder (Br (v, l, r)) = inorder l @ v :: inorder r;
> val 'a inorder = fn : 'a tree -> 'a list

- fun inorder tree =
    let fun f Lf a = a
          | f (Br (v, l, r)) a = f l (v :: f r a)
    in f tree []
    end;
> val 'a inorder = fn : 'a tree -> 'a list

We can insert all the elements from a list into a tree

- fun list2tree lst = foldl (fn (a,b) => insert b a) Lf lst;

If the list is in order, the resulting tree will effectively be a list as well.

Tree functionals

Some familiar functionals have natural analogues in trees

- fun maptree f Lf = Lf
    | maptree f (Br (v, l, r)) =
        Br (f v, maptree f l, maptree f r);
> val ('a, 'b) maptree = fn : ('a -> 'b) -> 'a tree -> 'b tree

- fun fold f e Lf = e
    | fold f e (Br (v, l, r)) =
        f (v, fold f e l, fold f e r);
> val ('a, 'b) fold = fn :
    ('a * 'b * 'b -> 'b) -> 'b -> 'a tree -> 'b

As with lists, many simple tree functions can be rewritten using functionals

- fun count t = fold (fn (_, l, r) => 1 + l + r) 0 t;
> val 'a count = fn : 'a tree -> int

- fun depth t = fold (fn (_, l, r) => 1 + Int.max(l, r)) 0 t;
> val 'a depth = fn : 'a tree -> int

Tree functionals

We can also write fold functionals that let us treat an ordered binary tree as a list

- fun foldltree f e Lf = e
    | foldltree f e (Br (v, l, r)) =
        foldltree f (f (v, foldltree f e l)) r;
> val ('a, 'b) foldltree = fn :
    ('a * 'b -> 'b) -> 'b -> 'a tree -> 'b

- fun foldrtree f e Lf = e
    | foldrtree f e (Br (v, l, r)) =
        foldrtree f (f (v, foldrtree f e r)) l;
> val ('a, 'b) foldrtree = fn :
    ('a * 'b -> 'b) -> 'b -> 'a tree -> 'b

This gives us an easy way to collect the elements from a tree into a list

- fun tolist t = foldrtree op:: [] t;
> val 'a tolist = fn : 'a tree -> 'a list

Using foldltree would give us the elements in reverse order. We could have done this with the normal tree fold, too, though not as efficiently

- fun tolist t = fold (fn (v, l, r) => l @ v :: r) [] t;
> val 'a tolist = fn : 'a tree -> 'a list

Dictionaries

A binary search tree can be extended easily to act as a dictionary mapping keys to values

- datatype ('a, 'b) dict = Lf | Br of 'a * 'b * 'a dict * 'a dict;

lookup should return a value or raise an appropriate exception if the key is not found, and insert should replace an existing value or add a new key/value pair.

The cipher from lecture 4 can easily be modified to use a binary tree dictionary instead of a list.