Imperative language features

Bad use of imperative language features

fun impFact (n:int) =
  let
   val result = ref 1
   val i = ref 0
   fun loop () =
      if (!i = n) then ()
      else (i := !i+1);
            result := !result * !i;
            loop())
   in  (loop(); !result)
   end
Point: factorial is really a pure function that does not need storage or assignment.

Good uses of storage

  1. Associating meters with function invocations.

    Suppose we want to be able to count how many times a function is invoked in the program.

    First attempt: wrap the function to produce another function that increments counter and then calls the function

    val counter = ref 0;
    
    fun wrap f = (counter := !counter +1; f)
    
    val wrapAdd = wrap add;
    
    wrapAdd 5 6
    
    !counter (*will return 1 *)
    
    wrapAdd 6 7
    
    !counter (*will return 1*)
    
    Problem: counter is only incremented when we wrap the function. We want it to be incremented whenever we invoke the function.

    Second attempt:

    fun wrap f = fn x => (counter := !counter +1; f x);
    
    val wrapAdd = wrap add;
    
    wrapAdd 4 5;
    
    Problem: this works correctly but we have only one counter for every function we want to wrap.

    Third attempt:

    fun wrap f =
            let
              val counter = ref 0
            in fn x =>  (counter := !counter + 1; f x)            
            end
    
    Problem: counter is not visible to the rest of the program, so we have no way to read counter value when we are done invoking the function!

    Fourth attempt:

    fun wrap f =
            let
              val counter = ref 0
            in (fn x => (counter := !counter + 1; f x),
                fn () => !counter
                )
            end
    
    fun add x y = x + y
    val (wrapAdd, readAdd) = wrap add
    readAdd()
    wrapAdd 3 4
    readAdd()
    ...  
     
  2. Mutable arrays
    val array: int * 'a => 'a array
    val length: 'a array => int
    val sub: 'a array * int =? 'a
    val update: 'a array * int * 'a -> unit
    
    val a = Array.array(10,0) (*array subscripts go from 0 to 9)
    val n = Array.array(a) 
    val e = Array.sub(a,5) (*may raise subscript out of bounds exception*)
    val _ = Array.update(a,4,3)
    
  3. Mutable lists:
    val l = [ref 0, ref 0, ref 0] (* type of l is int ref list *)
    hd(l) := 4
    l     (* will return [ref 4, ref 0, ref 0] *)
    hd(tl(l)) := !(hd(l)) + 4
    ....
    
  4. Implementing recursive functions with references

    From our previous discussions we know that we can only declare recursive functions using the fun, or val rec statements. We also know that we can use val statements to declare simple (non-recursive) functions. We show the power of references by implementing recursive functions using val statements.

    We illustrate our method using the canonical example of recursive functions, the factorial:

    fun fact(n: int): int = 
      if n = 0
      then 1
      else n * (fact(n - 1))
    
    If we were to use a val statement to declare fact, the declaration would fail because of the recursive call fact(n - 1). To avoid this problem, we replace the recursive call with a call to a function specified using a reference:
    val x: (int -> int) ref = ref (fn _: int => 1)
    
    val fact: int -> int = (fn (n: int) => if n = 0 then 1 else n * (!x)(n - 1))
    
    Function fact does not really implement the factorial yet:
               /
               | 1, if n = 0,
    fact(n) = -|
               | n, otherwise.
               \
    
    We can "convince" fact to implement the factorial by resetting reference x to fact itself:
    val () = x := fact
    
    With this transformation, we now have a correct implementation of the factorial function:
    - fact 0;
    val it = 1 : int
    - fact 1;
    val it = 1 : int
    - fact 3;
    val it = 6 : int
    - fact 10;
    val it = 3628800 : int