4. MATERIALES Y MÉTODOS
4.3. MODELACIÓN DE LA RED EN WATERCAD (V8i)
val r = ref 0 val s = ref 0
the variablesrandsare not aliases, but after the declaration val r = ref 0
val s = r
the variablesrandsare aliases for the same reference cell.
These examples show that we must be careful when programming with variables of reference type. This is particularly problematic in the case of functions, because we cannot assume that two different argument variables are bound to different reference cells. They might, in fact, be bound to the same reference cell, in which case we say that the two vari- ables arealiasesfor one another. For example, in a function of the form fn (x:typ ref, y:typ ref) => exp
we may not assume thatxandyare bound to different reference cells. We must always ask ourselves whether we’ve properly considered aliasing when writing such a function. This is harder to do than it sounds. Aliasing is a huge source of bugs in programs that work with reference cells.
13.5
Programming Well With References
Using references it is possible to mimic the style of programming used in imperative languages such as C. For example, we might define the facto- rial function in imitation of such languages as follows:
13.5 Programming Well With References 120
fun imperative 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
Notice that the functionloopis essentially just a while loop; it repeatedly executes its body until the contents of the cell bound to ireaches n. The tail call toloopis essentially just agotostatement to the top of the loop.
It is (appallingly) bad style to program in this fashion. The purpose of the function imperative fact is to compute a simple function on the natu- ral numbers. There is nothing about its definition that suggests that state must be maintained, and so it is senseless to allocate and modify storage to compute it. The definition we gave earlier is shorter, simpler, more effi- cient, and hence more suitable to the task. This is not to suggest, however, that there are no good uses of references. We will now discuss some im- portant uses of state in ML.
13.5.1
Private Storage
The first example is the use of higher-order functions to manage shared private state. This programming style is closely related to the use of ob- jects to manage state in object-oriented programming languages. Here’s an example to frame the discussion:
13.5 Programming Well With References 121
local
val counter = ref 0 in
fun tick () = (counter := !counter + 1; !counter) fun reset () = (counter := 0)
end
This declaration introduces two functions, tickof typeunit -> intand reset of type unit -> unit. Their definitions share a private variable counter that is bound to a mutable cell containing the current value of a shared counter. Thetickoperation increments the counter and returns its new value, and thereset operation resets its value to zero. The types of the operations suggest that implicit state is involved. In the absence of exceptions and implicit state, there is only one useful function of type unit->unit, namely the function that always returns its argument (and it’s debatable whether this is really useful!).
The declaration above defines two functions,tickandreset, that share a single private counter. Suppose now that we wish to have several differ- entinstancesof a counter — different pairs of functionstickandresetthat share different state. We can achieve this by defining acounter generator(or
constructor) as follows: fun new counter () =
let
val counter = ref 0
fun tick () = (counter := !counter + 1; !counter) fun reset () = (counter := 0)
in
{ tick = tick, reset = reset } end
The type ofnew counteris
unit -> { tick : unit->int, reset : unit->unit }.
We’ve packaged the two operations into a record containing two func- tions that share private state. There is an obvious analogy with class-based object-oriented programming. The functionnew countermay be thought of as aconstructorfor a class of counterobjects. Each object has a privatein- stance variablecounterthat is shared between themethodstickandreset of the object represented as a record with two fields.
13.5 Programming Well With References 122
Here’s how we use counters. val c1 = new counter () val c2 = new counter () #tick c1 (); (* 1 *) #tick c1 (); (* 2 *) #tick c2 (); (* 1 *) #reset c1 (); #tick c1 (); (* 1 *) #tick c2 (); (* 2 *)
Notice thatc1 and c2 are distinctcounters that increment and reset inde- pendently of one another.
13.5.2
Mutable Data Structures
A second important use of references is to build mutable data structures. The data structures (such as lists and trees) we’ve considered so far are
immutable in the sense that it is impossible to change the structure of the list or tree without building a modified copy of that structure. This is both a benefit and a drawback. The principal benefit is that immutable data structures arepersistentin that operations performed on them do not destroy the original structure — in ML we can eat our cake and have it too. For example, we can simultaneously maintain a dictionary both before and after insertion of a given word. The principal drawback is that if we aren’t really relying on persistence, then it is wasteful to make a copy of a structure if the original is going to be discarded anyway. What we’d like in this case is to have an “update in place” operation to build anephemeral
(opposite of persistent) data structure. To do this in ML we make use of references.
A simple example is the type ofpossibly circular lists, orpcl’s.Informally, a pcl is a finite graph in which every node has at most one neighbor, called its predecessor, in the graph. In contrast to ordinary lists the predecessor
13.5 Programming Well With References 123
relation is not necessarily well-founded: there may be an infinite sequence of nodes arranged in descending order of predecession. Since the graph is finite, this can only happen if there is a cycle in the graph: some node has an ancestor as predecessor. How can such a structure ever come into existence? If the predecessors of a cell are needed to construct a cell, then the ancestor that is to serve as predecessor in the cyclic case can never be created! The “trick” is to employ backpatching: the predecessor is initial- ized toNil, so that the node and its ancestors can be constructed, then it is reset to the appropriate ancestor to create the cycle.
This can be achieved in ML using the followingdatatypedeclaration: datatype ’a pcl = Pcl of ’a pcell ref
and ’a pcell = Nil | Cons of ’a * ’a pcl;
A value of type typ pcl is essentially a reference to a value of type typ
pcell. A value of typetyp pcellis eitherNil, the cell at the end of a non- circular possibly-circular list, or Cons (h, t), where h is a value of type
typandtis another such possibly-circular list.
Here are some convenient functions for creating and taking apart possibly- circular lists:
fun cons (h, t) = Pcl (ref (Cons (h, t))); fun nill () = Pcl (ref Nil);
fun phd (Pcl (ref (Cons (h, )))) = h; fun ptl (Pcl (ref (Cons ( , t)))) = t;
To implement backpatching, we need a way to “zap” the tail of a possibly- circular list.
fun stl (Pcl (r as ref (Cons (h, ))), u) = (r := Cons (h, u));
If you’d like, it would make sense to require that the tail of the Conscell be the empty pcl, so that you’re only allowed to backpatch at the end of a finite pcl.
Here is a finite and an infinite pcl.
val finite = cons (4, cons (3, cons (2, cons (1, nill ())))) val tail = cons (1, nill());
val infinite = cons (4, cons (3, cons (2, tail))); val = stl (tail, infinite)