SLIDE 1

Functional Programming and ML

Notes available on-line:
   
http://www.cs.binghamton.edu/~lander/cs571  

To a large extent the notes are based on the information in the excellent introduction: J.D.Ullman, Elements of ML Programming,  Prentice-Hall, 1994

ML is a strongly typed functional language that became available in the mid-80s.

ML stands for meta-language and was developed as part of a larger project under Robin Milner (Turing award winner 1992)

We emphasize the functional aspects of ML and will ignore features such as data encapsulation:


Notes

The book by Sebesta, Concepts of Programming Languages, Chapter 13, has some interesting comments on the beginning of Lisp. The book is on reserve. In the mid 50's, after their success with FORTRAN, IBM were trying to extend the language for symbolic processing. It had been shown at CMU (Allen Newell & Herb Simon) that symbolic processing was the key element for AI applications. In 1958, John McCarthy spent a summer in the IBM Lab, but did not like the approach. He went back to MIT and designed Lisp. Much later a much cleaner Lisp-like language called Scheme was developed for instructional use at MIT. Scheme is fairly widely used as a first programming language.

It is usually felt that a stongly typed language makes programming easier and helps develop better code. ML is strongly typed but, thanks to the kind of pattern-matching technology in Prolog, ML can discover most of the types introduced in the program. This type-discovery saves the programmer the trouble of declaring types in a program which many programmers find is a disadvantage of newer imperative languages.

The language ML is functional:

The ML we shall use is SML/NJ, a version of Standard ML, on bingsuns, running under Solaris. To use SML/NJ, log on and type sml. To get out of ML type CTRL-D.


SLIDE 2

Assignment ("-" is the prompt):

- val pi = 3.14159;
val pi = 3.14159 : real
- pi;
val it = 3.14159 : real

"it" is the result of the evaluation

Built-in operations:

- 5 * 7;
val it = 35 : int
- 5 * (3 + 12);
val it = 75 : int


Notes

Try typing:

- val pi = 3.14159;

We can assign a value to a symbol A using val A = B; where the value the expression B is assigned to A.In a functional language, we do not consider A to be a variable; it will usually remain constant during the execution of a function and it will often be constant during a whole program. The symbol pi will now evaluate to 3.14159, try typing

- pi;

Having set pi to 3.14159 above, and given that ML evaluates its input, typing pi returns 3.14159. Upper and lower case are significant in ML. Built-in operators such as div and mod are lower case.


SLIDE 3

ML has the basic built-in types:


Notes

Comments: comments are bracketed by (* and *). They can spread over several lines and can even be nested!

- ~3 - 4;
val it = ~7 : int
- ~5 * ~7;
val it = 35 : int

Explicit type conversions are possible:

- ~0.7071/real(2);
val it = ~0.35355 : real
- val S = "\n\ta\tb\n\
= \\t3\t4\n";
val S = "\n\ta\tb\n\t3\t4\n" : string

Note that the "=" symbol is the system prompt for inputs that stretch over more than one line. The first line has a "-" and all subsequent lines are prompted with "=".

- print(S);
     a     b
     3     4
val it = () : unit


SLIDE 4

Operations for booleans X and Y:

  • not X : bool -> bool
  • X andalso Y,
        X orelse Y : bool*bool->bool
  • Operations for integers X and Y:

  • ~X : int -> int
  • X + Y, X - Y, X * Y : int*int -> int
  • X div Y, X mod Y : int*int -> int
  • X = Y, X <>Y : int*int -> bool
  • X < Y, X <= Y : int*int -> bool
  • X > Y,  X >= Y : int*int -> bool
  • real(X) : int -> bool
  • Operations for reals X, Y:

  • ~X : real -> real
  • X + Y, X - Y : real*real -> real
  • X * Y, X / Y : real*real -> real
  • X = Y, X <>Y : real*real -> bool
  • X < Y, X <= Y : real*real -> bool
  • X > Y,  X >= Y : real*real -> bool
  • ceiling(X), floor(X) : real -> int
  • truncate(X) : real -> int
  • Operations for strings X and Y:

  • X ^ Y : string*string -> string
  • X = Y, X <>Y : string*string -> bool
  • X < Y, X <= Y : string*string -> bool
  • X > Y,  X >= Y : string*string -> bool

  • Notes

    - "Book" < "bag" andalso 5 <= 6 orelse 7.0 <> real(8);
    val it = true : bool
    - "Book" ^ "bag";
    val it "Bookbag" : string
    - ceiling(5.5) div 4;
    val it = 1 : int
    - floor(7.6) * 3;
    val it = 21 : int
    - floor(~7.6);
    val it = ~8 : int
    - truncate(~7.6);
    val it = ~7 : int


    SLIDE 5

    Defining functions:

    - fun sq(x:real) = x*x;
    val sq = fn: real->real
    - fun cubed(x:int) = x*x*x;
    val cubed = fn: int->int
    - fun power_6(x)
         = sq(real(cubed(x)));
    val power_6 = fn: real->real


    Notes

    fun is used in ML to introduce user-defined functions:

    - fun sq (x) = x * (x : int);
    val sq = fn : int -> int
    - fun cubed (x) = x * (x : int) * x;
    val cubed= fn : int -> int

    Functions can use other built-in or user-defined functions as in

    - fun power_4 (x) = sq (x * x);
    val power_4 = fn : int -> int

    Actually the parentheses are not necessary except for disambiguation. sq 4 and sq(4) give the same value. However, precedence rules are significant, sq x * x = (sq x)*x = sq(x)*x


    SLIDE 6

    Recursion (the "=" is a system prompt for incomplete input):

    - fun fact (n)
    =    if n=0 then 1
    =    else n*fact(n-1);
    val fact = fn : int->int

    Combined function definitions:

    - fun f1 = ...
    = and f2 = ...
    = and f3 = ...;


    Notes

    Functions are often recursive as in the famous factorial function above

    Recursion is used in most function definitions in a functional language. The "if" construct is one way to deal with the different cases. There is a more complex "case" construct we see later.

    An additional keyword (and) is used to handle groups of function definitions, especially mutual recursion. We give an example later.


    SLIDE 7

    The beauty of functional definitions is that they often model mathematical definitions exactly:
    factorial(n) = 1                              if n = 0
                         = n * factorial(n - 1) if n > 0

    Another recursive function is the Fibonacci function:
    fibonacci(n) = 1                                                          if n = 1, 2
                           = fibonacci(n - 1) + fibonacci(n - 2) if n > 2

    The problem with the ML code for the Fibonacci function in the Notes is that the number of recursive calls grows exponentially with n


    Notes

    Consider the not very efficient recursion:

    - fun fib(n) = if n = 1 orelse n = 2 then 1
    =                else fib(n-1)+fib(n-2);
    val fib = fn : int -> int

    Unfortunately the Fibonacci function above produces many recursive calls (the following is an expansion of the recursive computation of fib(7). The "=" sign is not the system prompt):

    fib(7) = fib(6)+fib(5) = fib(5)+fib(4)+fib(4)+fib(3)
    = fib(4)+fib(3)+fib(3)+fib(2)+fib(3)+fib(2)+fib(2)+fib(1)
    = fib(3)+fib(2)+fib(2)+fib(1)+fib(2)+fib(1)+1+fib(2)+fib(1)+1+1+1
    = fib(2)+fib(1)+1+1+1+1+1+1+1+1+1+1+1
    = 1+1+1+1+1+1+1+1+1+1+1+1+1 = 13

    Many things are known about the Fibonacci sequence. For example a formula for fib(n) is the following:

    fib(n) = [(1+sqrt(5))n - (1-sqrt(5))n]/2n*sqrt(5)

    where sqrt is the square-root function. The following relation is proved by induction and shows that fib(n) grows exponentially. Further, the number of recursive calls to evaluate fib(n) is larger than the value of fib(n) and hence the number of calls is exponential.

    (1.6)n-2 < fib(n) < (1.7)n-1 for n > 2

    There are more efficient ways to compute the Fibonacci function. First consider the function:

    - fun F(x, y) = (y, x + y);
    val F = fn : int * int -> int * int

    which is a function that takes a pair of integers as input and returns a pair of integer as output. We write Fn for n applications of the function F:

    Fn(x, y) = F(F(F ... F(x,y)...))

    It is not hard to see that fib(n) = #1(Fn(0,1)), for n > 0, where "#1" picks the first component of a pair. Just look at the sequence Fn(0,1) for n = 1, 2, 3, ... The sequence is (1,1), (1,2), (2,3), (3,5), (5,8), (8,13),... which is a sequence of pairs of successive Fibonacci numbers. Hence define

    - fun fb(n) = if n=0 then (0, 1)
    =             else F(fb(n-1));
    val fb = fn : int -> int * int

    Really fb(n) is exactly Fn(0,1).Then we could simply define

    - fun fib1(n) = #1(fb(n))
    val fib1 = fn : int -> int

    We shall see another efficient recursion later.


    SLIDE 8

    When there are too many options to define a function than can be handled using the if function, we can do the following:

    - fun gcd(0,m) = m
    =  | gcd (m,n) =
    =      if m >= n then
    =         gcd(m-n,n)
    =         else gcd(m,n-m);
    val gcd=fn:int*int ->int


    Notes

    We shall see the "case" construct later, which generalizes the "if" construct. To define functions you can use a sequence of alternaive patterns:

    fun f(<pattern 1>) = <definition 1>
    | f(<pattern 2>) = <definition 2>
    ...
    | f(<pattern k>) = <definition k>

    A few things are really important about patterns. The following version of the gcd function cannot be entered in ML because, unlike in Prolog, the same variable name cannot appear twice in the pattern part:

    - fun gcd(m,m) = m
    =  | gcd (m,n) = if m > n 
    =                then gcd(m-n,n) else gcd(m,n-m);
    Error: duplicate variable in pattern(s): n

    The problem is the pattern gcd(m,m). You are stuck with writing:

    - fun gcd(m,n) = if m = n then m
    =                 else if m > n then gcd(m-n,n) 
    =                               else gcd(m,n-m);
    val gcd = fn : int * int -> int

    Note that ML is also checking if all possible patterns are actually given. You get a warning if they are not, although you can still execute the function. We give examples later.

    Mistyped names can cause duplicate patterns and then erroneous execution. In the following, there is a letter O in place of the digit 0:

    - fun gcd(O,m) = m (* you probably cannot see the difference! *)
    =  | gcd (m,n) =
    =      if m >= n then
    =         gcd(m-n,n)
    =         else gcd(m,n-m);
    Warning: redundant patterns in match
         (O,m) => ...
         (m,n) => ...
    val gcd = fn : int * int -> int

    The problem is noticeable during execution since (O,m) matches (5,12), whereas (0,m) does not:

    - gcd(5,12);
    val it = 12 : int

    There is a "don't care" variable, denoted by an underscore ( _ ), which can appear more than once in a pattern and represents a different value each time. We shall use it later.


    SLIDE 9

    One of the basic data structures is the list (which may contain sublists):

    - val L1 = [1,4,10,3];
    val L1 = [1,4,10,3] : int list
    - [[1,2],[2],[6,1]];
    val it = [[1,2],[2],[6,1]] : int list list

    Notice that ML lists require homogeneous elements. For non-homogeneous structures you can use tuples ("abc", 7, 5.6) or their generalization "records"

    - val R1 = {
       field_name1 = val1,
       field_name2 = val2,
          ...
       field-nameN = valN
    };

    The types of the different fields can be anything, the fields can be introduced in any order and can be accessed by patterns in any order. We see examples later.


    Notes

    ML will give an error if you try to mix different types in a list. The tuples (and more generally records) can handle a mixture of types. Examples suggested by Ullman's book:

    - val tupl1 = (3, ~5.1, "ab" ^ "c");
    val tupl1 = (3, ~5.1, "ab" ^ "c") : int*real*string
    - val StuRec1 = {id = 12345678, Name="John Smith", courses=[571,573,550]};
    val StuRec1 = {Name="John Smith", courses=[571,573,550], id = 12345678}
        : {Name:string, courses:int list, id:int}

    Notice the field names are put into ASCII order.

    - #2(tupl1);
    val it = ~5.1 : real
    - hd(#courses(StuRec1));
    val it = 571 : int

    The function hd returns the first element in a list. The other important list function is tl which returns the rest of the list following the head.

    The use of "and"

    Now that we have introduced lists, consider a program where a function f has to be applied to the odd-numbered elements, i.e. the 1st, 3rd, 5th etc., and a function g has to be applied to the even-numbered elements, i.e. the 2nd, 4th, 6th etc. The intention is to return the same list with the functions f and g applied to the appropriate elements, e.g.

    - fun f(x) = x div 2;
    val f = fn : int -> int
    - fun g(x) = 2 * x;
    val g = fn : int -> int
    - fun F(nil) = nil
    =  | F(x::xs) = f(x)::G(xs)
    =  and G(nil) = nil
    =  | G(x::xs)= g(x)::F(xs);
    val F = fn : int list -> int list
    val G= fn : int list -> int list
    - F([6,8,3,5,1,9]);
    val it = [3,16,1,10,0,18] : int list

    Some of the list notation is described in the next slide.

    To give a not very useful example of the use of the pattern "_" suppose we want to write a function that returns true if a list has even length and otherwise returns false:

    - fun evenLength(nil) = true
    = | evenLength([ _ ]) = false
    = | evenLength( _ :: _ :: xs) = evenLength(xs);
    val evenLength = fn : 'a list -> bool


    SLIDE 10

    Testing list membership:

    - fun memfun(x,nil)=false
    = | memfun(x,y::ys)=
    =      if x=y then true
    =      else memfun(x,ys);
    val memfun=fn:''a*''a list -> bool

    An empty list is denoted nil.

    The notation x::xs means a list whose first element (the head) is x and whose remaining list of elements (the tail) is denoted by xs.

    The response by ML that this is an 'a list indicates that a list of any one type is acceptable.

    - memfun(1,[2,3,1,6,7])
    (* = memfun(1,[3,1,6,7])
       = memfun(1,[1,6,7])*);
    val it = true : bool


    Notes

    The function hd returns the "head" (first element) of a list and tl returns the "tail" or "rest" of a list (i.e. all the list except the first element).

    - hd([[1,2], [3,4], [5,6]])
    val it = [1,2] : int list
    - tl([[1,2], [3,4], [5,6]])
    val it = [[3,4], [5,6]] : int list list

    Hence we could have written:

    - fun memfun(x, nil) = false
    = | memfun(x, L) = (x = hd(L)) orelse memfun(x, tl(L))

    Notice that since we have to equate x with the head of L, the elements of L must be of a type that admits the operations = and <>. Such types are called equality types. Not all types are equality types, for example, function types are never quality types. To remind the user that the type of x, ML denotes its type as ''a. The two quotes signifies an equality type.

    We could not use memfun to see if a particular function is a member of a list of functions.

    Because it is not legal to have a variable name appear twice in a pattern we cannot write the function in this way:

    - fun memfun(x, nil) = false
    = | memfun(x, x::xs) = true
    = | memfun(x, _::xs) = memfun(x, xs);
    Error: duplicate variable in pattern(s): x

    The function just defined works as follows:

    - memfun(1,[2,3,1,6,7]);
    val it = true : bool
    - memfun(1,[2,3,6,7]);
    val it = false : bool
    - memfun(1,[]);
    val it = false : bool
    - memfun(1,nil)
    val it = false : bool


    SLIDE 11

    Anonymous functions.

    Lambda calculus depends on a notation for anonymous functions and functional programming languages are always partly inspired by lambda calculus.

    lambda x.(x*x) 4
    16

    and ML allows the following:

    - fn(x:int) => x*x;
    val it = fn : int -> int
    -(fn(x:int) => x*x) 4;
    val it = 16 : int


    Notes

    It is sometimes convenient to create an anonymous function at some position inside another function. The technique uses the "fn" construction:

    fn (parameter-list) => expression

    means "the function which takes 'parameter list' as input and returns the value of 'expression' after substitution of the parameters" e.g. an anonymous function which computes x3:

    fn (x:int) => x*x*x

    Such a function can be applied to an argument:

    - (fn (x:int) => x*x*x) 4;
    val it = 64 : int

    We will explore uses of lambda expressions later. We can use the notation to define named functions:

    - val sq = fn (x:int) => x * x
    val sq = fn : int -> int


    SLIDE 12

    "Let expressions" are important for more complex code:

    - fun comb1(n,r) =
    = let
    =   val factn = fact(n);
    =   val factr = fact(r);
    =   val  factn_r=fact(n-r)
    = in factn div
    =        (factr*factn_r)
    = end;
    val comb = fn int*int->int

    This is not a good way to compute nCr. The following is better:

    - fun comb2(_,0) = 1
    = | comb2(n,r) =
    =    if n=r then 1 else
    =      comb2(n-1,r)+
    =         comb2(n-1,r-1);
    val comb2=fn:int*int->int


    Notes

    Another linear-time computation of nCr is:

    - comb3(n,0) = 1
    = | comb3(n,r) = n * comb3(n-1, r-1) div r;
    val comb3 = fn : int * int -> int

    To simplify complicated expressions the let function may be used:

    - fun fib (1) = 1
    =  | fib(2) = 1
    =  | fib(n) = let val x1 = fib(n-1);
    =                 val x2 = fib(n-2)
    =             in x1 + x2
    =             end;
    val fib = fn : int * int -> int

    The temporary variables x1 and x2 are given the values of the expressions fib(n-1) and fib(n-2) and then x1 + x2 is evaluated. The general form is

    let val x1=E1; val x2=E2; ... val xk=Ek in F end;

    The assigning of Ei to xi is done in succession. Thus if x1 appears in E2, it is the new value of x1 (namely E1) that is used:

    let val x=2; val y=x in y end;
    val it = 2 : int


    SLIDE 13

    Basic list operations:

    - hd
    - tl
    - nth([1,2,3,4,5,6], 4);
    val it = 5 : int
    - nth([1,2,3,4,5,6], 0);
    val it = 1 : int
    - nthtail([1,2,3,4,5],2);
    val it = [3,4,5] : int list
    - nthtail([1,2,3,4,5],0);
    val it = [1,2,3,4,5]:int list
    - null
    - length
    - map
    - exists
    - @


    Notes

    - length(["a","b","c","d","e"]);
    val it = 5 : int

    Note that length could be coded as:

    - fun length(nil) = 0
      | length(x::xs) = 1 + length(xs);
    val length = fn : 'a list -> int

    or, using the boolean test for nil, called null:

    - fun length(x) = if null(x) then 0 else 1 + length(tl(x));
    val length = fn : 'a list -> int

    The function map maps a list and a function to a new list where the function has been applied to each element of the input list:

    - map sq [1,2,3,4];
    val it = [1,4,9,16] : int list

    Note that you DO NOT use parentheses when you use map as we shall explain later.

    The function exist takes a list and a boolean function as input. The whole function returns true if the boolean function is true on at least one element of the list:

    - fun even(x) = if x mod 2 = 0 then true else false;
    val even = fn : int -> bool
    - exist even [1,2,3,4,5];
    val it = true : bool
    - exist even [1,3,5];
    val it = false : bool

    The symbol "@" is used to concatenate two lists:

    - [1,2,3] @ [4,5,6]
    val it = [1,2,3,4,5,6] : int list


    SLIDE 14

    Creating a stack:

    - val create_stack = nil;
    val create_stack=[]:'a list
    - fun push_stack(x,s)=x::s;
    val push_stack=fn:'a*'a list-> 'a list
    - fun top_stack(x)=hd(x);
    val top_stack=fn:'a list -> 'a
    - fun pop_stack(x)=tl(x);
    val pop_stack=fn:'a list -> 'a list


    Notes

    It is because the elementary list operations work at the start of the list that implementing a stack is very easy:

    Usage:

    - val My_stack = create_stack;
    val My_stack = [] : 'a list
    - My_stack;
    val My_stack = [] : 'a list
    - val My_stack = push_stack(1,Mystack)
    val My_stack = [1] : int list
    - val My_stack = push_stack(2,Mystack)
    val My_stack = [2,1] : int list
    - val My_stack = push_stack(3,Mystack)
    val My_stack = [3,2,1] : int list
    - top_stack(My_stack);
    val it = 3 : int
    - val My_stack = pop_stack(Mystack);
    val My_stack = [2,1] : int list

    Non-nested lists operate like stacks. Here you should retrieve the top element of the stack before popping the stack. We can use "@" to implement a queue:

    - val create_queue = nil;
    val create_queue = [] : 'a list
    - fun add_queue(x,s) = s @ [x];
    val add_queue = fn : 'a * 'a list -> 'a list
    - fun front_queue(x) = hd(x);
    val front_queue = fn : 'a list -> 'a
    - fun delete_queue(x)=tl(x);
    val delete_queue = fn : 'a list -> 'a list


    SLIDE 15

    Naive reverse

    - fun reverse1(nil)=nil
    = | reverse1(x::xs)=
    =     reverse1(xs)@[x];
    val reverse1=fn:'a list->'a list


    Notes

    "Naive reverse" is a simple algorithm. In Lisp, it is inefficient because the operation equivalent to "@" makes O(length(xs)) calls to a memory allocation operation. Overall the Lisp equivalent of "reverse-1" makes O(length(xs)*length(xs)) calls to memory allocation.

    For Lisp, a more efficient version uses an accumulating parameter. The number of calls to the memory allocator is O(length(x::xs)).


    SLIDE 16

    An accumulating parameter and one or more auxiliary functions often make recursive programs more efficient.

    - fun rev_aux(x,acc_prm)=
    =   if null(x)
    =   then acc-prm
    =   else
    =    rev_aux(tl(x),
    =         hd(x)::acc_prm);
    val rev-aux=fn:'a list->'a list
    - fun reverse2(x)=
    =        rev_aux(x,nil);
    val reverse2=fn:'a list->'a list


    Notes

    The function call reverse2(L) makes length(L) calls to the list constructor "::"

    - reverse2 ([1,2,3,4]);
    (* =rev_aux([1,2,3,4], nil)
       =rev_aux([2,3,4], [1])
       =rev_aux([3,4], [2,1])
       =rev_aux([4], [3,2,1])
       =rev_aux(nil, [4,3,2,1]) *)
    val it = [4,3,2,1] : int list

    Now we can make factorial even simpler using an auxiliary function and accumulating parameter:

    - fun fact_aux(M,N) = if M=0 then N else fact_aux(M-1, M*N);
    val fac_aux = fn : int * int -> int
    - fun fact1(N) = fact_aux(N,1);
    val fact1 = fn : int -> int

    The function fact_aux is tail-recursive and we have discussed in class that such code can be converted to efficient loops during compilation.

    A much more efficient computation of the Fibonacci numbers uses two accumulating parameters:

    - fun fib_aux(PrePrev,Prev,Num) =
    =    if Num = 1
    =    then Prev
    =    else fib_aux(Prev, PrePrev+Prev, Num-1)
    - fun fibonacci1(N) = fib_aux(0,1,N)

    This version is also tail-recursive.


    SLIDE 17

    Higher-order functions:

    - map fact [1,7,10];
    val it=[1,5040,3628800]:int list

    An implementation of a similar function can be written in ML:

    - fun map1(f,x)
    =  if null(x)
    =  then nil
    =  else
    =   f(hd(x))::map1(f,tl(x));
    val map1=fn:('a->'b)*'a list->'b list

    The real map function is defined a bit differently.


    Notes

    Consider the filter map. This function applies to a whole list, a function argument that works on individual elements of the list:

    - fun sq(x:int) = x * x;
    val sq = fun : int -> int
    - map1(null, [[], [a], [b,c]]);
    val it = [true, false, false] : bool list
    - map1(sq, [2,4,6]);
    val it = [4,16,36] : int list
    - map null [[], [a], [b,c]];
    val it = [true, false, false] : bool list
    - map sq [2,4,6];
    val it = [4,16,36] : int list

    Consider also

    - map1(fn(x)=>x*x, [2,4,6]);
    val it = [4,16,36] : int list
    - map (fn(x)=>x*x) [2,4,6];
    val it = [4,16,36] : int list

    Here we are saying "apply to each element of the list [2,4,6] the function which takes x to x*x." The function map is often called higher-order because it takes a function as an argument. It is also an iterator since it iterates through a list doing something to each element.

    A similar function is filter. This function removes elements from the list which is the second argument if they do not satisfy the property given by the first argument (which is a boolean-valued function, i.e. a predicate):


    SLIDE 18

    - fun filter(p,nil)=nil
    = | filter(p,x::xs)=
    =    if p(x)
    =    then x::filter(p,xs)
    =    else filter(p,xs)
    val filter=fn:'a list->'a list

    Example:

    - fun even(n) = (n div 2 = 0);
    val even = fn:int->int
    - filter(even,
    =       [1,2,3,4,5,6,7]);
    val it = [2,4,6] : int list


    Notes

    Other examples:

    - fun not_null(x) = not (null(x));
    val not_null = fn : 'a list -> 'a list
    - filter(not_null,[[],[3.1],[2.6,7.4]]);
    val it = [[3.1],[2.6,7.4]] : real list list
    - filter(fn(x) => memfun(x,["a","b","c"]), ["d", "a", "g", "b", "t"])
    val it = ["a", "b"] : string list

    Editing while in ML


    SLIDE 19

    Datatype

    We can create user-defined datatypes as follows:

    datatype (<param-list>) 
     <id> =
      <1st contructor expr> |
      <2nd contructor expr> |
       ...                  |
      <n-th contructor expr>

    Examples

    - datatype student =
    =  undergrad |
    =  masters |
    =  doctoral;
    datatype student
    con undergrad : student
    con masters : student
    con doctoral : student
    - fun isUG(x) =
    =     (x = undergrad);
    val fun=fn:student->bool
    - fun graduate(x) =
    =   case x of
    =    undergrad =>
    =      print("Bachelors Degree")
    =  | masters =>
    =      print("Masters Degree")
    =  | doctoral =>
    =      print("Ph.D. Degree");
    val graduate=fn:student->unit


    Notes

    There may not be a <param-list>, in which case there are not parentheses. If there is only one parameter in the <param-list>, then the parentheses are not needed.

    - isUG(undergrad);
    val it = true : bool
    - isUG(doctoral);
    val it = false : bool
    - isUG(high_school);
    Error: unbound variable : high_school

    The function print(X) will work if X is of type int, real, string, bool but ML will have to be told which type X has whenever it cannot figure it out itself. Ullman's book has the following example:

    - fun printHead(nil) = print("Bad luck, empty list")
    =  | printHead(x::xs) = print(x:int);
    val printHead = fn : int list -> unit

    The case statement:

    case <expression> of
       <pattern1> => <expression1> |
       <pattern1> => <expression2> |
       ...                         |
       <pattern1> => <expressionN>

    If the list of patterns do not cover all possible values of <expression>, then ML will issue the Warning: match not exhaustive. You can still execute such a case statement but if <expression> then takes a value that does not match any of the patterns, the exception Match is raised. There is a "don't care" variable, denoted by an underscore ( _ ) which provides a pattern that matches any value:

    - fun binDigit(x : int) =
    =   case x of
    =      0 => print("zero") |
    =      1 => print("one")  |
    =      _ => print("not a binary digit");
    val binDigit = fn : int -> unit


    SLIDE 20

    - datatype 'a tree =
    = Empty  |
    = Node of 'a * 'a tree list
    (* could be written
       ('a)*(('a tree) list) *);
    datatype 'a tree =
    con Empty : 'a tree |
    con Node:'a*'a tree list -> 'a tree
    - fun prtLeftBranch
    =    (x : int tree) =
    =  case x of
    =    Empty => print("\n")
    = |  Node(y,[]) =>
    =      (print(y);
    =       print("\n"))
    = |  Node(y,z::_) =>
    =      (print(y);
    =       print("\n");
    =       prtLeftBranch(z));
    val prtLeftBranch=fn:int tree -> unit


    Notes

    If there is one parameter for the datatype, there is no need to put parentheses around it. Hence we create a general tree containing the data from an unspecified type, denoted by the type variable 'a. This is a general tree and a node can have any number of child nodes, each one the root of a subtree. The list of trees is the list of all the subtrees under the particular node in question.

    In the function prtLeftBranch we need to be able to print the values of the nodes. In this case, we choose to define the function for a tree of integers. It only prints the left branch of the tree.

    Notice that we can get a sequence of actions by writing (expr-1; expr-2;...; expr-n). The value of this whole expression is the value of the last expression. When the last expression is a print command, then the value is unit.


    SLIDE 21

    - datatype number =
    =    I of int |
    =    R of real;
    datatype number =
    con I : int -> number|
    con R : real -> number
    - fun plus(x,y) =
    =  case (x,y) of
    =   (I(x),I(y))=> I(x+y)
    =  |(I(x),R(y))=> R(real(x)+y)
    =  |(R(x),I(y))=> R(x+real(y))
    =  |(R(x),R(y))=> R(x+y);
    val plus=fn:number*number->number


    Notes

    This construction gives us a way to form numbers as a union of integers and reals. It is awkward to use however:

    - plus(R(3.9),I(7));
    val it = R(10.9) : number


    SLIDE 22

    References: ALGOL 68 placed emphasis on "ref" types (pointers). The indication "ref" became one of the uses for "*" in C

    - val Ptr1 = ref 7;
    val Ptr1 = ref 7 : int ref
    - val X = 3;
    val X = 3 : int
    - val Ptr2 = ref X;
    val Ptr2 = ref 3 : int ref

    The reference variable must have a concrete (specific) type. You cannot reference something whose type contains a type variable.

    Reference variables can be assigned to! (After they have been defined)

    - Ptr1 := 7;
    val Xptr = ref 7 : int ref

    Reference variables can be dereferenced:

    - !Xptr;
    val it = 7 : int

    SLIDE 23

    The creation of functions attached to variables is a way to create objects with a set of methods attached to achieve an object-oriented style:

    - fun BankAcct(balance:int)
    =  let 
    =   val BalPtr = ref balance
    =  in
    =   fn(action amount)
    =   => case action of
    =    "deposit" =>
    =      (BalPtr := BalPtr + amount;
    =       !BalPtr)
    =   |"withdraw" =>
    =      (BalPtr := BalPtr - amount;
    =       !BalPtr)
    =   |"balance" => !BalPtr;
    val BankAcct=fn:int->(string*int->int)
    - val Acct1 = BankAcct(500);
    val Acct1=fn:string*int->int
    - Acct1("balance",0);
    val it = 500 : int
    - Acct1("withdraw",475);
    val it = 25 : int


    Notes

    The most significant aspect of this construct is that each variable assigned to BankAcct(n) gets its own copy of the local variable Bal_Ptr. Although this is a local variable of the call to the function BankAcct, the local variable has to persist even after the function call ends!

    The value returned by BankAcct is a function string * int -> int which references Bal_Ptr. That pointer is used to store the balance of the bank account. You can see that each call to BankAcct generates a separate variable Bal_Ptr from the following code:

    - val Acct1 = BankAcct(500);
    val Acct1 = fn : string * int -> int
    - val Acct2 = BankAcct(1000);
    val Acct2 = fn : string * int -> int
    - Acct1("deposit",50);
    val it = 550 : int
    - Acct2("deposit",50);
    val it = 1050 : int
    - Acct1("balance",0);
    val it = 550 : int
    - Acct2("balance",0);
    val it = 1050 : int


    SLIDE 24

    Currying functions

    It has been important in the development of functional notations as a model of computation to concentrate on functions of one variable.

    John Curry did considerable work in the area. The idea is often used today in mathematics.

    A function

    f:X*Y -> Z

    can be thought of as a function

    fc:X -> (Y -> Z)

    where (Y -> Z) is the set of functions from Y to Z

    The idea is: fc(x) is the function given by

    fc(x)(y) = f(x,y)


    SLIDE 25

    The same idea works for any number of variables:

    f:U*V*X*Y -> Z

    corresponds to

    fc:U->(V->(X->(Y->Z)))

    fc(u) is a function V->(X->(Y->Z)); fc(u)(v) is a function X -> (Y -> Z); fc(u)(v)(x) is a function Y -> Z, given by fc(u)(v)(x)(y) = f(u,v,x,y).


    SLIDE 26

    Earlier we saw the functions map and map1. In fact, map1c = map:

    - fun map(f) =
    = let 
    =  fun tmp(nil) = nil
    = | tmp(x::xs)=
    =           f(x)::tmp(xs)
    = in tmp
    = end;
    val map=fn:('a ->'b)->'a list -> 'b list