Immutable, singly linked lists are part of the core language. Elements in a given list must be of a single type.
[]
[elt1, elt2, …,
eltn]
elt :: list
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 :: []
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
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.
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.
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.
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.
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]
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
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
while
loopNot 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
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
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.