Lecture 19: Side Effects, Arrays, Memory

Administrivia

PS#5 is now out. It will be released in 2 parts; the first one is out already. Please start on it ASAP, it is long.

PS#4 regrades: students should not be penalized for timeouts in the test harness.

Arrays in SML

An important kind of mutable data structure that SML provides is the array.  The type t array is in fact very similar to the Java array type t[].  Arrays generalize refs in that they are a sequence of mutable cells containing values.  We can think of a ref cell as an array of size 1.  Here's a partial signature for the builtin Array structure for SML. 

  signature ARRAY =
    sig
    (* Overview: an 'a array is a mutable fixed-length sequence of
     * elements of type 'a. *)
      type 'a array
 
    (* array(n,x) is a new array of length n whose elements are
     * all equal to x. *)
      val array : int * 'a -> 'a array
   (* fromList(lst) is a new array containing the values in lst *)
      val fromList : 'a list -> 'a array
      (* indicates an out-of-bounds array index *)
      exception Subscript 
      (* sub(a,i) is the ith element in a. If i is
       * out of bounds, raise Subscript *)
      val sub : 'a array * int -> 'a
      (* update(a,i,x)
       * Effects: Set the ith element of a to x
       * Raise Subscript if i is not a legal index into a *)
      val update : 'a array * int * 'a -> unit
      (* length(a) is the length of a *)
      val length : 'a array -> int
 
      ...
    end

See the SML documentation for more information on the operations available on arrays.

Notice that we have started using a new kind of clause in the specification, the effects clause. This clause specifies side effects that the operation has beyond the value it returns. When a routine has a side effect, it is useful to have the word "Effects:" in the specification to explicitly warn the user that A side effect may occur. For example, the update function returns no interesting value, but it does have a side effect.

An imperative update to a mutable data abstraction is also known as a destructive update, because it "destroys" the old value of the data structure. An assignment to an array element changes the array in place, destroying the old sequence of elements that formerly made up the array. Programming in an imperative style is trickier than in a functional style precisely because the programmer has to be sure that the old value of the mutable data is no longer needed at the time that a destructive update is performed.

Array examples

- val arr = array(3,42);
val arr = [|42,42,42|] : int array
- update(arr,2,24);
val it = () : unit
- arr;
val it = [|42,42,24|] : int array
- sub(arr,2);
val it = 24 : int
 

Question: why do we need arrays when we have tuples?

Wrong answer: because we can change arrays.

Demonstration:

- val tp = (ref 1, ref 2);
val tp = (ref 1,ref 2) : int ref * int ref
- #1(tp) := 42;
val it = () : unit
- tp;
val it = (ref 42,ref 2) : int ref * int ref
 

Right answer: arrays allow us to index via an expression, which tuples don’t. # doesn’t evaluate its arguments (although, of course, you could change this in mini-ML…)

How to think about refs

It is possible to have a formal model for ref’s, and in fact we will do this in a future lecture. However, for the moment, here is a good way to think about a ref cell.

Recall how when we did environment diagrams we had “unboxed” values (things like closures that were not written in the binding, but instead pointed to). A ref cell is a box that points to a value (the value could actually be another ref!)

ref(e1) creates a new cell

!cell gets what cell points to

e1 := e2 evaluates e1, and make that ref cell point to (the value of) e2. Note that it changes where the ref cell points, not what it points to!

Another example:

val x = [ref 1,ref 2, ref 3]
 
val a = hd x;
val b = tl x;
 
a := 7;
hd(b) := 14;
 
 
- val ex1 = ref 3;
val ex1 = ref 3 : int ref
- val ex2 = ref ex1;
val ex2 = ref (ref 3) : int ref ref
- ex1 := 42;
val it = () : unit
- ex1;
val it = ref 42 : int ref
- ex2;
val it = ref (ref 42) : int ref ref
- !ex2 := 24;
val it = () : unit
- ex1;
val it = ref 24 : int ref
- ex2;
val it = ref (ref 24) : int ref ref
 

Circular datastructures

datatype Lst = Nil | Cons of int*(Lst ref)
 
fun take(n:int,l:Lst):int =
  case l of
   Nil => ~1
 | Cons(a,b) => if (n = 1) then a else take(n-1,!b)
 
val y = Cons(1,ref Nil);
let
  val Cons(first,rest) = y
in
  rest := y;
  y
end
 

Why we need circular datastructures

let val f = fn(x) => f(x+1) in E end
let fun f(x) = f(x+1) in E end
let fun f(x) = g(x+1) and g(y) = f(y-1) in E end
 
 
(* let val f = fn(x) => f(x+1) *)
 
let
  val cl = makeclosure(x,f(x+1),TOP)
  val b = makebinding("f", cl)
  val env = addbinding(b,TOP)
in
  env
end
 
 
(* let fun f(x) = f(x+1) *)
 
let
  val b = makebinding("f", JUNK)
  val env = addbinding(b,TOP)
  val cl = makeclosure(x,f(x+1),env)
in
  bash(binding, cl);
  env
end
 
 
(* let fun f(x) = g(x+1) and g(y) = f(y-1) *)
 
let
  val b1 = makebinding("f", JUNK)
  val b2 = makebinding("g", JUNK)
  val env = addbinding(b1,addbinding(b2,TOP))
  val c1 = makeclosure(x,g(x+1),env)
  val c2 = makeclosure(y,f(y-1),env)
in
  bash(b1, cl);
  bash(b2, c2);
  env
end

Memory allocation and de-allocation issues