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.
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.
- 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…)
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
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) = yin
rest := y; yend
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
envend
(* 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); envend
(* 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); envend