CS 312 Lecture 15
Using references

ML allows you to allocate and manipulate mutable storage cells. Here are a few simple examples:

val r = ref 0    (*create an int cell initialized to 0 *)
val s = ref 0    (*create an int cell initialized to 0 *)   
val _ = r:= 3    (*update value of cell pointed to by r to 3 *)
val x = !s + !r  (*adds values in cells pointed to by s and t*)
val t = r        (*t and r now point to the same cell *)
val _ = t := 5   (*update cell pointed to by t (and r) *)
val y = !s + !t
In the presence of references, the aliasing problem arises: two different "names" for the same storage cell can be created. Aliasing makes it difficult to reason about programs, as shown in the following example:
  fun f (a:int ref, b: int ref) = (a := !a +1;  !b)

  val s = ref 0
  val t = s
  f(s,t)  (*will return 1*)

  

Implementing counters using references

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 count f = (counter := !counter +1; f)

val add' = count add;

add' 5 6

!counter (* will return 1 *)

add' 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 count f = fn x => (counter := !counter +1; f x);

val add' = count add;

add' 4 5;
Problem: this works correctly but we have only one counter for every function we want to wrap.

Third attempt:

fun count 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 count f =
        let val counter = ref 0
        in (fn x => (counter := !counter + 1; f x),
            fn () => !counter)
        end

fun add x y = x + y
val (add', read_counter) = count add
read_counter()
add' 3 4
read_counter()
...  
 

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. That is, a val declaration of function doesn't allow us to recursively call (or refer to the function) in the declaration. 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

Bad use of imperative language features

References can be used to implement imperative-style mutable variables, by creating one ref cell per variable. However, writing imperative-style programs in a functional language such as ML represents bad use of the language. The imperative-style factorial function below illustrates the misuse of references:

fun imp_fact (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.