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
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
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
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.
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 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
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 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.
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
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
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.
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
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
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.