Introduction to Functional Programming

Russ Ross

Computer Laboratory
University of Cambridge
Lent Term 2005


Lecture 4

Polymorphic types

In parametric polymorphism, a function or datatype is general enough to work with objects of different types. Such polymorphic types represent a family of concrete types; a type variable in the type description can be replaced with a concrete type to realize a monomorphic instance of the more general type.

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

SML instantiates types as needed.

- fun remove _ [] = []  
    | remove elt (x::xs) =                 
        if x = elt then remove elt xs
        else x :: remove elt xs;
> val ''a remove = fn : ''a -> ''a list -> ''a list

- remove 3;
> val it = fn : int list -> int list

- remove "hi";
> val it = fn : string list -> string list

Equality types are the subset of types that can be tested for equality using = and are denoted with two quote marks.

Polymorphism and static types

All types are determined statically; there is no runtime polymorphism. A polymorphic type can be instantiated in multiple ways, but all type variables for a given instance must be instantiated as a group.

- fun f x y = ();
> val ('a, 'b) f = fn : 'a -> 'b -> unit

- val g = f ();
> Warning: Value polymorphism:
! Free type variable(s) at top level in value identifier g
> val g = fn : 'a -> unit

- g "hi";
> Warning: the free type variable 'a has been instantiated to
  string
> val it = () : unit

- g 0;
> Toplevel input:
! g 0;
!   ^
! Type clash: expression of type
!   int
! cannot have type
!   string

- f 1;
> Warning: Value polymorphism:
! Free type variable(s) at top level in value identifier it
> val it = fn : 'b -> unit

A simple cipher

We can define a monoalphabetic substitution cipher using simple recursive functions. Our dictionary will be a list of pairs (ab) where string a is mapped to string b. First we define a function to split a string into a list of strings, each containing one character

- fun split s = 
    let val lst = explode s
        fun c2s [] = []
          | c2s (x::xs) = str x :: c2s xs
    in
      c2s lst
    end;
> val split = fn : string -> string list

Cipher continued

To define the cipher we need a function to create a dictionary from a pair of strings, where a character in the input string is translated to a character in the output string. We use zip from a previous lecture

- fun makedict from to =
    zip (split from, split to);
> val makedict = fn : string -> string -> (string * string) list

The next function takes a dictionary list and uses it to translate a single string

- fun lookup [] c = c
    | lookup ((a, b) :: xs) c =
        if a = c then b
        else lookup xs c;
> val ''a lookup = fn : (''a * ''a) list -> ''a -> ''a

Cipher continued

The encoder translates each string, then joins the strings together to give a final encoded result

- fun encodelist dict lst =
    let fun convert [] = []
          | convert (x::xs) = lookup dict x :: convert xs
        fun merge [] s = s
          | merge (x::xs) s = merge xs (s^x)
    in
      merge (convert lst) ""
    end;
> val encodelist =
    fn : (string * string) list -> string list -> string

The decoder inverts the dictionary and then uses encodelist

- fun decodelist dict lst =
    let fun invert [] = []
          | invert ((a, b) :: xs) = (b, a) :: invert xs
    in
      encodelist (invert dict) lst
    end;
> val decodelist =
    fn : (string * string) list -> string list -> string

Local bindings

Just as we can do let bindings within functions, we can define local bindings when defining toplevel functions and other values.

We create a dictionary, then use encodelist and decodelist to instantiate a specific encoder and decoder

- local 
    val dict = makedict "abcdefghijklmnopqrstuvwxzy"
                        "qwertyuiopasdfghjklzxcvbnm"
  in
    val encode = encodelist dict o split
    val decode = decodelist dict o split
  end;
> val encode = fn : string -> string
  val decode = fn : string -> string

Recall that o is the infix compose operator. encode is the function that applies split to its argument, then applies encodelist dict to the result.

- encode "the gold is under the big X";
> val it = "zit ugsr ol xfrtk zit wou X" : string

- decode it;
> val it = "the gold is under the big X" : string

Note: decode evaluates invert dict every time it is used.

Functionals

Many functions that operate on lists have the same form. The recursive structure of the list dictates the form of the function that processes it.

- fun scale _ [] = []
    | scale n (x::xs) = n * x :: scale n xs;
> val scale = fn : int -> int list -> int list

- fun product [] = 1
    | product (x::xs) = x * product xs;
> val product = fn : int list -> int

Using higher-order functions, we can generalize the most common forms of list operations into all-purpose list functionals.

Map

It is often useful to systematically transform each element of a list to yield a new list as in the scale example, or the split function from our encoder. In general, we map the elements of one list into the elements of a new list using a transform function

- fun map _ [] = []
    | map f (x::xs) = f x :: map f xs;
> val ('a, 'b) map = fn : ('a -> 'b) -> 'a list -> 'b list

Lambda functions are useful for simple mappings

- fun scale n lst =      
    map (fn x => x * n) lst;
> val scale = fn : int -> int list -> int list

Lists of pairs can be mapped using standard operators

- fun addvectors (v1, v2) =
    map op+ (zip (v1, v2));
> val addvectors = fn : int list * int list -> int list

If the types already match, we can be clever and compose functions

- val addvectors = map op+ o zip;
> val addvectors = fn : int list * int list -> int list

Fold

Iteration over a list can be generalized as foldl (starting at the left or head of the list) or foldr (starting at the right or tail of a list).

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

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

When folding a list, we compute a single value by folding each new list element into the result so far, with an initial value provided by the caller.

- fun product lst = foldl op* 1 lst;
> val product = fn : int list -> int
  Note: Could be val product = foldl op* 1;

- fun rev lst = foldl op:: [] lst;
> val 'a rev = fn : 'a list -> 'a list
  Note: NOT val rev = foldl op:: [];

- fun map f lst = foldr (fn (x, a) => f x :: a) [] lst;
> val ('a, 'b) map = fn : ('a -> 'b) -> 'a list -> 'b list

Note that foldl is tail recursive and foldr is not.

Other functionals

filter returns a list containing only those elements of its input list that satisfy a predicate

- fun filter _ [] = []
    | filter f (x::xs) =    
        if f x then x :: filter f xs
        else filter f xs;
> val 'a filter = fn : ('a -> bool) -> 'a list -> 'a list

exists tests if any element of the list satisfies a predicate. all tests if every element satisfies a predicate

- fun exists _ [] = false
    | exists f (x::xs) = (f x) orelse exists f xs;
> val 'a exists = fn : ('a -> bool) -> 'a list -> bool

- fun all _ [] = true
    | all f (x::xs) = (f x) andalso all f xs;
> val 'a all = fn : ('a -> bool) -> 'a list -> bool

This example tests whether two lists have no common elements

- fun disjoint xs ys =
    all (fn x => all (fn y => x <> y) ys) xs;
> val ''a disjoint = fn : ''a list -> ''a list -> bool

The option type

The built-in option type lets us encode a value if it exists, or a placeholder value if it doesn't.

- fun safediv x y =     
    if y = 0 then NONE
    else SOME (x div y);
> val safediv = fn : int -> int -> int option

- fun sum NONE x = SOME x
    | sum (SOME a) x = SOME (x + a);
> val sum = fn : int option -> int -> int option

- NONE;
> val 'a it = NONE : 'a option

- sum it 1;
> val it = SOME 1 : int option

- sum it 2;
> val it = SOME 3 : int option

- sum it 3;
> val it = SOME 6 : int option

- sum it 4;
> val it = SOME 10 : int option

Cipher using functionals

We can rewrite our encoder without using any explicit recursion

-  
val split = map str o explode
 
fun makedict from to =
  zip (split from, split to)
 
fun lookup dict c =
  case List.find (fn (a, b) => a = c) dict of
    SOME (a, b) => b 
  | NONE => c 
 
fun encodelist dict =
  foldl op^ "" o map (lookup dict)
 
val decodelist =
  encodelist o map (fn (a, b) => (b, a));

> val split = fn : string -> string list
  val makedict = fn : string -> string -> (string * string) list
  val ''a lookup = fn : (''a * ''a) list -> ''a -> ''a
  val encodelist =
      fn : (string * string) list -> string list -> string
  val decodelist =
      fn : (string * string) list -> string list -> string

Cipher using functionals

As before, we instantiate specific encode and decode functions

- local 
    val dict = makedict "abcdefghijklmnopqrstuvwxzy"
                        "qwertyuiopasdfghjklzxcvbnm"
  in
    val encode = encodelist dict o split
    val decode = decodelist dict o split
  end;
> val encode = fn : string -> string
  val decode = fn : string -> string

This time, decode only inverts the dictionary once.