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.
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
We can define a monoalphabetic substitution cipher using
simple recursive functions. Our dictionary will be a list of
pairs (a, b)
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
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
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
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.
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.
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
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.
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 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
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
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.