Princeton University
Computer Science Dept.

Computer Science 441
Programming Languages
Fall 1998

Lecture 3

More ML

Efficiency

Updating of data structures is based on sharing (where possible):

Ex.: If define

   - fun updatehd newhd (head::tail) = newhd :: tail;
then get sharing:

Safe, because list elt's not updatable!

Examples

recursion

QuickSort
   fun partition (pivot, nil) = (nil,nil)   
     | partition (pivot, first :: others) =   
       let 
           val (smalls, bigs) = partition(pivot, others)   
       in   
           if first < pivot then (first::smalls, bigs)   
                            else (smalls, first::bigs)   
       end;
The system responds with:
   
   val partition = fn : int * int list -> int list * int list
Not polymorphic since system assumes "<" is on integers. Can force to be function on reals (for example) by including type of pivot in declaration: (pivot:real,nil)
   
   fun qsort nil = nil   
     | qsort [singleton] = [singleton]   
     | qsort (first::rest) =   
        let 
           val (smalls, bigs) = partition(first,rest)   
        in  
           qsort(smalls) @ [first] @ qsort(bigs)   
        end;
It's hard to believe quicksort could be so simple!

Can make quicksort polymorphic if pass in less than operator to both partition and qsort:

   fun partition (pivot, nil) (lessThan) = (nil,nil)   
     | partition (pivot, first :: others) (lessThan) =   
       let 
          val (smalls, bigs) = partition(pivot, others) (lessThan)   
       in   
          if (lessThan first pivot) then (first::smalls, bigs)   
                                    else (smalls, first::bigs)   
       end;   
   
   > val partition = fn : ('a * ('b list)) ->    
		(('b -> ('a -> bool)) -> (('b list) * ('b list)))   
   
   fun qsort nil lessThan = nil   
     | qsort [singleton] lessThan = [singleton]   
     | qsort (first::rest) lessThan =   
        let    
           val (smalls, bigs) = partition(first,rest) lessThan   
        in     
           (qsort smalls lessThan) @ [first] @ (qsort bigs lessThan)   
        end;   
   
   > val qsort = fn : ('a list) -> (('a -> ('a -> bool)) -> ('a list))
Now if define:
   - intLt (x:int) (y:int) = x < y;   
   - qsort [6,3,8,4,7,1] intLt;   
   > val [1,3,4,6,7,8] : int list
Note: could define
   - val PIntLt :int * int -> bool = op <;
but wrong type for what needed here (though it is trivial to rewrite partition to take this type of function)!

Simulating iterative programs

Can simulate iterative programs in functional language by making local variables into parameters.

Ex. Obvious recursive def in ML:

   - fun fib 0 : int = 1   
       |  fib 1 = 1   
       |  fib n = fib (n-2) + fib (n-1);
Iterative solution in Pascal - faster!
   Function fastfib (n:integer):integer;   
   val a,b : integer;   
   begin   
      a := 1; b := 1;   
      while n > 0 do   
      begin   
         a := b; b := a + b; n := n-1  (* all done in parallel, else wrong! *)   
      end;   
      fib := a   
   end;

ML equivalent

   fun fastfib n = let    
         fun fibLoop a b 0 = a   
           | fibLoop a b n : int = fibLoop  b (a+b) (n-1)   
     in    
         fibLoop 1 1 n   
     end;   

Defining new types

User-defined types possible.

type

Type abbreviations use keyword type.
   type point = int * int	(* nullary *)   
   type 'a pair = 'a * 'a	(* unary *)

datatype

Generate new types using datatype.

Types are disjoint unions (w/constructors as tags)

Support recursive type definitions!

Generative (support pattern matching as well)

   - datatype color = Red | Green | Blue;   
   datatype color = Blue | Green | Red   
     con Red = Red : color   
     con Green = Green : color   
     con Blue = Blue : color

"con" stands for constructor.

Write constructor tags with capital letter as convention to distinguish from variables.

   - datatype 'a tree = Niltree | Maketree of 'a * ('a tree) * ('a tree)   
   
   datatype 'a tree = Maketree of 'a * ('a tree) * ('a tree)    
                         | Niltree   
   con Niltree = Niltree : 'a tree   
   con Maketree = fn : ('a * ('a tree) * ('a tree)) ->    
										                           ('a tree)

Write binary search program using trees!

	fun insert (new:int) Niltree = Maketree (new,Niltree,Niltree)   
	  | insert new (Maketree (root,l,r)) =    
	           if new < root then Maketree (root,(insert new l),r)   
	                            else Maketree (root,l,(insert new r))   
	   
	fun buildtree [] = Niltree   
	  | buildtree (fst :: rest) = insert fst (buildtree rest)   
	   
	fun find (elt:int) Niltree = false   
	  | find elt (Maketree (root,left,right)) =    
	            if elt = root then true   
	                          else if elt < root then find elt left   
	                          else find elt right  (* elt > root *)   
	   
	fun bsearch elt list = find elt (buildtree list);   
	   
	- buildtree [8,3,6,8,3,4,9,10];   
	Maketree (10,Maketree (9,Maketree (4,Maketree (3,Niltree, 
	Maketree (3,Niltree,Niltree)),Maketree (8,Maketree (6,Niltree,Niltree),
	Maketree (8,Niltree,Niltree))),Niltree), Niltree) : int tree   
	   
	- bsearch 4 [8,3,6,8,3,4,9,10];   
	true : bool   
	- bsearch 7 [8,3,6,8,3,4,9,10];   
	false : bool   
	   
	fun sumtree Niltree = 0   
	  | sumtree (Maketree(root,left,right)) = root + sumtree left + sumtree right;

Can also have any kind of tagged unions:

   - datatype sum = IntTag of int | RealTag of real | ComplexTag of real * real;

Abstract data types - later

Lazy vs. Eager evaluation

At some cost in execution efficiency, can add extra power to language by supporting lazy evaluation - also called call-by-need or normal order evaluation.

Order of operations:

Ex.

    - fun test (x:{a:int,b:unit}) =    
             if (#a{a=2,b=print("A\n")} = 2)    
                then (#a x)    
                else (#a x);   
    val test = fn : { a:int, b:unit } -> int 
      
    - test {a = 7, b = print("B")};
If have eager evaluation, get:
    BA   
    val it = 7 : int
If have lazy evaluation, get:
    val it = 7 : int
Call-by-need is equivalent to call-by-name (see discussion of parameter passing techniques later in course ) in functional languages, but can be implemented more efficiently since when evaluate argument, can save value (since it won't change).

Can also share different instances of parameter.

E.g.,

    fun multiple x = if x = [1,2,3] then 0::x else x@[4,5,6]
When substitute in value for x, don't really need to make three copies (again, since can't change!)

Lazy evaluation allows programmer to create infinite lists.

Ex. (in lazy dialect of ML)

    fun from n = n :: from (n+1)   
    val nats = from 1;   
    fun nth 1 (fst::rest) = fst   
      | nth n (fst::rest) = nth (n-1) rest
Can get approximate square root of x by starting with an approximation a0 for sqrt and then getting successive approximations by calculating

an+1 = 1/2 * (an + x/an)

Program infinite list of approximations by:

    fun approxsqrts x =   
       let 
          from approx = approx :: from (0.5 * (approx + x/approx))   
       in 
          from 1.0   
       end;
If want approximation where difference between successive approximation is < eps,
    fun within eps (approx1 :: approx2 :: rest) =   
        if abs(approx1 - approx2) < eps    
                then approx1   
                else absolute eps (approx2::rest);
Now to get square root approximation in which the difference between successive terms is < eps then write:
    fun sqrtapprox x eps = within eps (approxsqrts x)
Of course can also do with eager language, but bit more to worry about - must combine logic of both approxsqrts and within into same function.

Why not just use lazy evaluation?

Eager language easier and more efficient to implement w/ conventional techniques.

If language has side-effects, then important to know when they will occur!

Also many-optimizations involve introducing side-effects into storage to save time.

In parallelizing computation, often better to start computation as soon as ready. With eager evaluation, evaluation of parameter may be wasted.

Can simulate lazy evaluation in eager language by making expressions into "parameterless" functions.

I.e., if wish to delay evaluation of E : T, change to fn : () => E of type unit -> T.

Ex: Spose wish to implement second parameter of f with lazy evaluation:

    f x y = if x = [] then [] else x @ y
Rewrite as
    f' x y' = if x = [] then [] else x @ (y' ())  (* note y' applied to element of type unit *)
If would normally write:

f E1 E2, instead write f' E1 (fn () => E2)

Then E2 only evaluated if x != []!

Implement lazy lists, Suspended lists, in an eager language:

    datatype 'a susplist = Mksl of (unit -> 'a * 'a susplist) | Endsl;
Like regular list, but must apply to () before get components!

    (* add new elt to beginning of suspended list *)   
    fun slCons newhd slist = let fun f () = (newhd, slist)      
                             in Mksl f end;   
       
    exception empty_list;   
       
    (* extract head of suspended list *)   
    fun slHd Endsl = raise empty_list   
      | slHd (Mksl f) = let val (a,s) = f ()   
                        in a end;   
       
    (* extract tail of suspended list *)   
    fun slTl Endsl = raise empty_list   
      | slTl (Mksl f) = let val (a,s) = f()   
                        in s end;   
       
    (* Is suspended list empty? *)   
    fun slNull Endsl = true   
      | slNull(Mksl f) = false;   
         
    (* Infinite list of ones as suspended list *)   
    val ones = let fun f() = (1,Mksl f)   
               in Mksl f end;   
                  
    (* Suspended list of increasing integers starting with n *)   
    fun from n = let fun f() = (n, from(n+1))   
                 in Mksl f end;   
                    
    val nat = from 1;

Languages like LISP and SCHEME as well as lazy languages support streams for I/O.


CS441 | CS Department | Princeton University