Start Lecture #8

**Remark**: Lab2 has not yet been assigned.
The first question will be to redo lab1 in ML.

This section uses material from Ernie Davis's web page in addition to the sources previously mentioned.

ML was developed by Robin Milner et al. in the late 1970's. It was originally developed for writing theorem provers. We will be most interested in its type system, especially in its ability to use type inference so that we get static typing even though the programmer need very few type declarations. Instead the ML system deduces the types from the context!

We will be using Standard ML of New Jersey

, which you invoke
by typing sml.
ML has the following characteristics

- Functional, expresion-based programming language. Like Scheme in some respects, but the syntax is completely different. It will look more familiar than Scheme.
- Functions are first-class values.
- Interpreted environment.
- The system has a read—evaluate—print loop as in Scheme and other Lisps.
- The user types in an expression and the system responds
with the resulting value
**PLUS**the inferred types. - The user also types in variable definitions and function definitions and again the system responds with the inferred types.

- Rich type system with strong, static, typing.
However types are (usually)
**inferred**rather than declared. - Parametric polymorphism. The same function definition can be overloaded to be used for multiple types (somewhat like Ada generics).
- Structural equivalence.
- Lexically scoped.
- Built-in pattern matching (like Perl and Prolog).
- Garbage collection.
- Module system.
- Exceptions.
- Datatypes. Similar to enumerated literals + variant records.
- References.
Similar to
const pointers

. - Comments are bracketed by (* ... *)
- Overloading but
**no**coercion.

- 7+5 ; val it = 12 : int - val x = 7+5; val x = 12 : int

- 7 = + = 5; val x=12 : int

- 2.0+3.5; val it = 5.5 : real

- 2 + 3.5; stdIn:4.1-10.7 Error: operator and operand don't agree [literal] operator domain: int * int operand: int * real in expression: 2 + 3.5

As with Scheme, you can use ML as a desk calculator. You notice right away that the notation is conventional infix. Other simple characteristics include.

- The system keeps reading an expression until you type
a semicolon, so you can spread it over several lines. The prompt
changes from - to =.

- Basic types include int, real, bool, string, char.

- 7, -0.2, true/false, "Bob", #"B".

- The value calculated is assigned to variable
it

if nothing else specified.

- Val acts like Scheme's
`(define`.

- [2,3,4,5]; val it = [2,3,4,5] : int list - [[2.0,3.2], [], [0.0]]; val it = [[2.0,3.2],[],[0.0]] : real list list - [2.2,5]; stdIn:8.4-9.8 Error: operator and operand don't agree [literal] operator domain: real * real list operand: real * int list in expression: 2.2 :: 5 :: nil - [1,[2,4]]; stdIn:1.1-4.2 Error: operator and operand don't agree [literal] operator domain: int * int list operand: int * int list list in expression: 1 :: (2 :: 4 :: nil) :: nil - 2::[3,5]; val it = [2,3,5] : int list - hd([2,3,5])); val it = 2 : int; - tl([2,3,4]); val it = [3,4] : int list - [2,3]@[5,7]; val it = [2,3,5,7] : int list - []; val it = [] : 'a list

We will study lists, tuples, and records, emphasizing the first two.

**Lists**:
As in Scheme lists will prove to be important.

- ML Lists are written with [].

- they are homogeneous (all elements are of the same type)
unlike Scheme lists.
This is important for type inference.

- Note the type of each list (some are lists of lists).

- Note the error messages when the types don't agree.

- Lisp cons is ::, car is hd, cdr is tail.

- ML uses @ for concatenation.

- Note carefully the use of 'a in the last example.
It represents a type variable.
In that example the point is that the empty list can be a list
of anything, i.e., the type of the elements has not yet been
determined so we give it a letter name 'a.
It is an example of a
**polymorphic**list.

- val x = (2, 7.5, "Bob"); val x = (2,7.5,"Bob") : int * real * string #2 x; val it = 7.5 : real

**Tuples**:
Tuples are of fixed length (no concatenation), and can have
heterogeneous elements.
They are written with parentheses and their elements are selected by
index.

Tuples will also prove to be important. Formally, all procedures take only one parameter, but that parameter can be a tuple and hence it is easy to fake multiple parameters.

- val bob = { age=32, firstName="Robert", lastName="Jones", weight=183.2}; val bob = {age=32,firstName="Robert",lastName="Jones",weight=183.2} : {age:int, firstName:string, lastName:string, weight:real}

- #age bob; val it = 32 : int

**Records**:
Records are tuples with named fields.
Records are written with braces and elements are selected by giving
the field name.

- if (1 < 2) then 3 else 4; val it = 3 : int

- if (1 < 2) then 3 else 4.0; stdIn:38.26-39.26 Error: types of if branches do not agree [literal] then branch: int else branch: real in expression: if 1 < 2 then 3 else 4.0

Naturally some kind of if-then-else is provided; in ML it is actually if-then-else.

Note that the value of the `then` arm must have the same
type as the value of the `else` arm.
Again this is important for type inference.

- fun add3 I = I+3; val add3 = fn : int -> int - add3 7; val it = 10 : int - add3 (7); val it = 10 : int

- fun times(X,Y) = X*Y; val times = fn : int * int -> int - times(2,3); val it = 6 : int

- val t = (2,3); val t = (2,3) : int * int - times t; val it = 6 : int

- fun sum3(x,y,z) = x+y+z; val sum3 = fn : int * int * int -> int - sum3(5,6,7); val it = 18 : int

Functions are of course crucial since ML is a functional language.

It is important to note the type of each function on the right and see that ML can determine it without user intervention.

- add3 is a function that maps an integer to an integer.

- times takes a tuple of 2 integers to an integer.

- sum3 takes a tuple of 3 integers to an integer.

All ML functions take only one argument. Functions of multiple arguments are actually functions of the corresponding tuple. The tuple can be written with () as in the (2,3) example or a tuple variable can be defined as we did as well.

Although not shown, a function can also return a tuple.

Note that ML responds to a fun definition with a val and a fn.
As mentioned before `val` is akin to `(define` and now
note that `fn` is akin to `(lambda`.

- fun sumTo(N) = if (N=0) then 0 else N+sumTo(N-1); val sumTo = fn : int -> int - sumTo 10; val it = 55 : int

fun member(X,L) = if (L=[]) then false else if hd(L)=X then true else member(X,tl(L)); stdIn:49.24 Warning: calling polyEqual stdIn:49.54 Warning: calling polyEqual val member = fn : ''a * ''a list -> bool - member(5, [1,5,3]); val it = true : bool

- fun nth(I,L) = if (I=1) then hd(L) else nth(I-1,tl(L)); val nth = fn : int * 'a list -> 'a - nth (3, ["When", "in", "the", "course"]); val it = "the" : string

fun f(X) = if X <= 2 then 1 else g(X-1) + g(X-2) and g(X) = if X <= 3 then 1 else f(X-1) + f(X-3); - f(20); val it = 2361 : int

Iteration is carried out using recursion, as in Scheme.

Again make sure you understand how ML has determined the type. For example in nth, the 1 says int and [] or hd says list

Mutually recursive functions have to be defined together
as shown in the last example.

val (x,y) = (1,2); val x = 1 : int val y = 2 : int

- val x::y = [1,2,3]; val x = 1 : int val y = [2,3] : int list

- val x::y::z = [1,2,3]; val x = 1 : int val y = 2 : int val z = [3] : int list

Patterns in ML are similar to Perl or Prolog, though much less powerful than either of those.

The idea is that it matches a constructor with variables against an object and uses the type of the object to figure out the types of the constituents of the constructor.

On the right we see examples of matching tuples and lists.

Note that for both list examples only one matching is possible.

You can give multiple possible parameters for a function with a separate body for each. When the function is invoked, ML pattern matches the argument against the possible parameters and executes the appropriate body. I find this very cute.

- fun fib 1=1 | fib 2=1 | fib N = fib(N-1) + fib(N-2); val fib = fn : int -> int - fib 10; val it = 55 : int

- fun doubleList [] = [] | doubleList L = 2*hd(L)::doubleList(tl(L)); val doubleList = fn : int list -> int list - doubleList [2,3,5]; val it = [4,6,10] : int list

- fun append ([] , ys) = ys = | append (x::xs, ys) = x::append (xs, ys); val append = fn : 'a list * 'a list -> 'a list

- fun last [h] = h | last (h::t) = last t; stdIn:18.3-18.38 Warning: match nonexhaustive h :: nil => ... h :: t => ... val last = fn : 'a list -> 'a - last [2,3,5]; val it = 5 : int - fun last [h] = h | last (h::t) = last t | last [] = 0; val last = fn : int list -> int

The first example is fairly simple: we do different things for different integer values of N.

The second example separates out the empty list. Note that in both cases the result is a (possibly empty) list of integers. ML has inferred that L is an integer since we multiply it by 2.

Write `doubleList` in Scheme on the board.

The `append` function illustrates how you divide lists into
empty and those that are the cons of an element and a (smaller)
list.

The function `last` is perhaps more interesting.
It is polymorphic as it can take a list of anything (note that ML
declares its argument to be of type `'a list` and it
return value to be of type `'a`.

Note the warning that the match was nonexhaustive (i.e., not all
cases were covered).
The missing case is when h=[].
last([]) cannot be [] since that is of type `'a` list not
`'a`.
If you arbitrarily say `last` [] = 0, you lose
polymorphism!

- fun twoToN(N) = if N=0 then 1 else let val p = twoToN(N-1) in p+p end; twoToN = fn : int -> int - twoToN 5; val it = 32 : int

let val J=5 val I=J+1 val L=[I,J] val Z=(J,L) in (J,I,L,Z) end; val it = (5,6,[6,5],(5,[6,5])) : int * int * int list * (int * int list)

As mentioned above ML is lexically scoped.
For this to be useful, we need a way to introduce a lexical scope.
Like Scheme ML uses `let` for this purpose.

The syntax is different, of course.
The object-name-and-value pairs are written as
definitions and appear between `let` and `in`.
Following `in` comes the body and finally `end`.

The ML `let` is actually much closer to `let*` in
Scheme in that the bindings are done sequentially.
The second example illustrates the sequential binding.
If the first two val's were done in the reverse order, an error
results since J would not have been defined before it was used.

fun reverse(L) = let fun reverse1(L,M) = if L=[] then M else reverse1(tl(L),hd(L)::M); in reverse1(L,[]) end; val reverse = fn : ''a list -> ''a list

- reverse [3,4,5]; val it = [5,4,3] : int list

As you would expect, in addition to nested blocks
(`let` above), ML supports nested functions.
Once again `let` is used to indicate the nested scope.

One use of nested functions is to permit the definition of
helper

functions as in the reverse

code on the right.

Note that this implementation of reverse

is tail recursive.

Do you see why tail recursion is aided by applicative-order
reduction?

fun applyList(f,[]) = [] | applyList(f,h::t) = f(h)::applyList(f,t); val applyList = fn : ('a -> 'b) * 'a list -> 'b list

fun add3(n) = n+3; val add3 = fn : int -> int

- applyList(add3,[2,3,5]); val it = [5,6,8] : int list

fun sum(f,l,u) = if u<=l then 0 else f(u) + sum(f,l,u-1); val sum = fn : (int -> int) * int * int -> int

As with Scheme, ML functions are first class values. They can be created at run-time, passed as arguments, and returned by functions.

**Passing Functions as Arguments**:
Compare applyList on the right to the FILTER function you wrote in
Scheme for lab1.

Note that applyList is **not** tail-recursive; its
last act is not the recursive call, but instead a ::.

It needs a helper function applyList1(f,done,rest).
It is different from reverse1 because now we are building the result
from left to right not right to left.
You can't cons an element onto the right of a list; instead you do
concatenation.

- val f = fn x => x+1; val f = fn : int -> int

- sum(fn x => x*x*x, 0,10); val it = 3025 : int

- (fn x => x*x*x) (6); val it = 216 : int

**Anonymous Functions**:
Our use of `fun` is similar to using both
`(define` and `(lambda` in Scheme.
We could separate fun into its two parts as shown in the
first example on the right, `val`, which is like
`(define`, and `fn...=>`, which is like
`lambda`.

Looking back at our previous uses of `fun`, we see that ML
responds by using `val` and `fn`.

Sometimes we don't need a name for the function, i.e., it is
anonymous.
This usage is illustrated in the second and third examples, with the
anonymous function `x ^{3}`.

- fun adder(N) = fn X => X+N; val adder = fn : int -> int -> int

- adder 7 5 val it = 12 : int - adder (7,5); stdIn:1.1-3.2 Error: operator and operand don't agree [tycon mismatch] operator domain: int operand: int * int in expression: adder (7,5)

- val add3 = adder 3; val add3 = fn : int -> int - add3 7; val it = 10 : int

**Returning Functions as Values**
On the right we see a simple example of one function
**adder** returning another (anonymous) function as a
result.
Note the type ML assigns to `adder`.
The → operator is right associative so the type says that
`adder` takes an integer and returns a function from integers
to integers.

Write `adder` on the board in Scheme and note the double
lambda.

Having functions return functions gives an alternative method for
defining multi-variable functions.
This method, known as Currying

, after Haskell Curry, is
illustrated on the right.
Make sure you understand why `adder 7 5` works and
why `adder(7,5)` gives a type error!

We can define functions like `add3` above in terms of
`adder`.

val add3 = let val I=3 in fn X => X+I end; val add3 = fn : int -> int add3 7; val it = 10 : int;

**Closures**:
As in Scheme it is possible to form a closure; that is, to include
the local environment.
In the simple case on the right, the environment is just the value
of the identifier `I`.
With the limited subset of ML that we are studying, closures do not
play an important role.

If you are interested, see the end of lecture 5 where I just added a short section on closures in Scheme. Naturally it is optional for this semester.

As mentioned several time, type inference is an important feature of ML, one that distinguishes it from all the languages we have see so far.

Recall the following characteristics of the ML type system.

- The base types are: int, real, bool, string, char.

- A list of objects of type T has type T list
- [3,4,5] is of type int list.
- [4.0, 5.2] is of type real list.
- [[3,4],[],[6,7,8]] is of type int list list, i.e., a list of integer lists.
- [] is of type 'a list, a list of objects of indeterminate
type 'a.
Note that although the type is not determined it is
constant, i.e., a list is homogeneous.
Types such as
`'a`,`'b`,`''a`, etc are*type variables*are signify that any type can appear, but all occurrences of a single type variable must be bound to the same type.

- A tuple of objects of types T1, T2, ..., Tk has type
T1 * T2 * ... * Tk.
- ("Bob",3,4.2) has type
`string * int * real`. - (3, [4,5]) has type
`int * int list`. - [(3,4),(3,5)] has type
`(int * int) list`.

- fun add3 I = I+3; val add3 = fn : int -> int

- fun times(X,Y) = X*Y; val times = fn : int * int -> int

fun member(X,L) = if (L=[]) then false else if hd(L)=X then true else member(X,tl(L)); val member = fn : ''a * ''a list -> bool

- fun nth(I,L) = if (I=1) then hd(L) else nth(I-1,tl(L)); val nth = fn : int * 'a list -> 'a

fun applyList(f,[]) = [] | applyList(f,h::t) = f(h)::applyList(f,t); val applyList = fn : ('a -> 'b) * 'a list -> 'b list

fun sum(f,l,u) = if u<=l then 0 else f(u) + sum(f,l,u-1); val sum = fn : (int -> int) * int * int -> int

fun adder(N) = fn X => X+N; val adder = fn : int -> int -> int

fun reversePair(x,y) = (y,x); val reversePair = fn : 'a * 'b -> 'b * 'a - ("Bob",3,4.2) has type
- A function mapping an object of type U to an object of type V
has type fn : U -> V.
- The first two examples are fairly clear.
`add3`is a function taking integers to integers and`times`is a function taking pairs of integers to integers. - The third example is a little more complicated.
`member(X,L)`takes as argument a tuple`(X,L)`where`X`has type`''a`and`L`has type`''a list`and returns a Boolean. So in this example, whatever the type of`X`,`L`must be a list of objects of that same type. This makes sense since we want to see if`X`is a member of`L`. - nth takes as argument a tuple
`(I,L)`where`I`is an integer and`L`is a list of objects of type`'a`and returns an object of type`'a`. ML figures this out since 1 is known to be an integer and`hd`is known to take a list as argument. `applyList(f,L)`is different yet. It's first argument`f`is an arbitrary function (that is, both the argument to`f`and the value calculated by`f`can of arbitrary types and those types need not be the same). However, its second argument`L`must be a list of objects of the same type as the argument of`f`. Finally, the value of`applyList`has the same type as the does the value of`f`.`sum`is a little tricky. The 0 says`sum`returns integer and the + says that`f`does so as well. The - says u is an integer, and the`f(u)`says that`f`takes integer arguments. The comparison says`u`and`l`have the same type. Putting this together gives the final type for`sum`.- We already discussed
`adder`and mentioned that -> is right associative. `reversePair`naturally reverses the types as well.

- The first two examples are fairly clear.

**Why Does ML Do It?**
That it, what are the advantages of type inference.

- It frees the programmer from writing types.
In addition to the clear pragmatic advantage, there is a
theoretical one as well:
There are functions whose types are
*exponentially*larger that the function itself. We will see one such function below. - With type inference, you get polymorphism for free.
- One body works for all types and no need to indicate what
types it applies to when writing the body.
For example, consider the
`member`function above. - This one body works for
*infinitely many*types. - No need to indicate which version you wish to apply at a given point.

- One body works for all types and no need to indicate what
types it applies to when writing the body.
For example, consider the

**How Does ML Do It?**
It is not magical, but is not trivial either!
I believe the most common name for the procedure adopted by ML and
other type-inferring languages is the Hindley-Milner Algorithm, but
it is also sometimes referred to as Algorithm W or the Damas-Milner
Algorithm.

We won't try to prove that it always works, but do note some of the facts (i.e., constraints) the algorithm uses.

- The operands of many built-in operators have (partially) known
types.
For example
`hd(X)`implies that`X`has type`'a list`and both operands of + must be of the same (arithmetic) type. - The branches of a conditional must have the same type.
- The condition of a conditional must be a Boolean.
- If X of type T is an element of list L of type U, then U = T list
- ... a few others like these.

The algorithm then proceeds very roughly as follows.

- If the type signature of an object is known, then label it with the signature. Else label it with a separate type parameter.
- Use the constraints to establish/deduce the type of one object from another. Note that this may involve binding one type parameter to be a type expression involving some other type parameter.
- For overloaded operators like "+", try to infer the type from the arguments; else assume that these are integer functions.
- Each different occurrence of [] in the program requires a different type parameter. One may be the base of a list of integers, and the other may be the base of a list of strings.

For a detailed discussion of type inference in ML, see Ben Goldberg's class notes (for Honors PL), Polymorphic Type Inference

**Example 1**:
Draw my diagram on the board.

fun sum(f,l,u) = if u<=l then 0 else f(u) + sum(f,l,u-1); val sum = fn : (int → int) * int * int → int

Assign type(u)='a, type(l)='b, type(f)='c→'d, type(sum)='g →'h.

(Syntactically, f has to be a function.)

Since sum can return 0, of type int, 'h=int.

Using the expression u-1, since - is either of type int*int→int or real*real→real, and type(1)=int, infer 'a=int.

Using the expression f(u), infer 'c=int.

Using the expression u<l, since < is either of type int*int→bool or real*real→bool, and type(u)=int, infer 'b=int.

Using the expression f(u) + sum(f,l,u-1), since + is either of type int*int→int or real*real→real, and 'h=type(sum(...))=int,'d=int.

Note that 'g=type(f,l,u)=(int→int)*int*int.

**Example 2**:
There is an alternate formulation below.

fun applyList(f,l) = if (l=[]) then [] else f(hd(l))::applyList(f,tl(l));

Assign type(l)='a, type(f)='b→'c, type(applyList)='d→'e, type([])='g list, type(hd)='h list→'h, and type(tl)='i list→i.

From the expression l=[], since [] is of type 'g list, deduce that 'a='g list.

Since hd is of type 'h list→'h, and l is of type 'g list, infer the expression hd(l) has type 'g. Hence infer that 'b='g.

Since tl is of type 'i list→'i list and l is of type 'g list, infer that tl(l) is of type 'g list.

Since :: is of type 'j*'j list→'j list, and f(hd(L)) is of type 'c, infer that f(hd(l))::tl(l)) is of type 'c list.

Since applyList may return f(hd(l))::tl(l), infer that 'e='c list.

Thus applyList is of type (fun 'g→'c) * 'g list→'c list.

**An Alternate Formulation of Example2**

Object | Type |
---|---|

l | 'a |

f | 'b→'c |

applyList | 'd→'e |

1st[] | 'g list |

hd | 'h list→'h |

tl | 'i list→'i |

:: | 'j * 'j list → 'j list |

2nd[] | 'k list |

Code Seg | Implication | |
---|---|---|

l=[] | => | 'a = 'g list |

hd(l) | => | 'h list = 'a = 'g list => 'h = 'g |

f(hd(l)) | => | 'b = 'h = 'g |

tl(l) | => | i' list = 'a = 'a list =>; 'i = 'g |

f(...):: | => | 'j = 'i |

can return :: | => | 'e = 'j list = 'i list = 'c list |

arg=(f,l) | => | 'd = ('b→'c) * 'a list = ('g→'c) *g list |

Hence applyList='d→'e = (('g→'c) * 'g list)→'c list |

fun applyList(f,l) = if (l=[]) then [] else f(hd(l)):: applyList(f,tl(l));

fun largeType(w,x,y,z) = x=(w,w) andalso y=(x,x) andalso z=(y,y); val largeType = fn : ''a * (''a * ''a) * ((''a * ''a) * (''a * ''a)) * (((''a * ''a) * (''a * ''a)) * ((''a * ''a) * (''a * ''a))) -> bool

**Example 3**:
This weird example shows that it is **possible** for
the size of a type to be exponential in the size of the function.

The operator `andalso` is the normal Boolean and

.
Recall that `and` has another meaning in ML.
Similarly `orelse` is the normal Boolean or

**Homework:** CYU 54.
Explain how the type inference of ML leads naturally to polymorphism.

**Homework:** CYU 57 How do lists in ML differ from
those of Lisp and Scheme?

**Homework:** Write an ML program that accepts a list
of strings and responds by printing the first string in the list.
If the list is empty it prints "no strings".

**Homework:** Write an ML program that accepts a list
of integers and responds by printing the first integer in the list.
If the list is empty it prints "no integers".