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.