** 13. Side Effects and References ** ---------------------------------------------------------------------------- 13.1. ML supports side-effects through explicit constructs, refs and arrays. Variables and memory cells are not mutable (no analog to set! or set-car!). The way to get a mutable memory cell is to create a ref value. Refs support 3 main operations: ref v => create a new ref cell, initialized to hold the value v !r => extra & return the current contents of ref cell r r := v => update the contents of ref cell r to be the value v Ex: val counter = ref 0; (* val counter: int ref *) counter := !counter + 1; counter := !counter + 1; !counter (* val it:int = 2 *) Refs can be shared, creating aliases: val counter2 = counter; (* val counter2: int ref *) counter := !counter + 1; counter2 := !counter2 + 1; !counter (* val it:int = 2 *) !counter2 (* val it:int = 2 *) Refs can hold anything, including functions and other refs; orthogonality! ---------------------------------------------------------------------------- 13.2. Core ML can be extended with refs. First, we add a new type, ref: tau ::= ... | ref ref is a new polymorphic type, that given a type returns the type of references to that type. We use the polymorphic ref type by instantiating it, e.g. ref[int], ref[ref[bool]], .... At the value level, we add predefined functions to the initial environment (Gamma), mkref (the same as the ML ref function, but renamed to avoid using the same name as the ref type), !, and :=, with the following types: mkref: forall a. a -> ref[a] ! : forall a. ref[a] -> a := : forall a. ref[a] * a -> unit (Analogously to how + et al. are treated as predefined functions in the initial environment.) Nothing else needs to be done, at least in the typechecking rules. ---------------------------------------------------------------------------- 13.3. Type inference for refs in ML seems easy. Just treat them like anything else. E.g.: val r = ref 0 (* infer that r: int ref *) What if the value in the ref is polymorphic? E.g.: val r = ref (fn x => x) (* a ref to the identity function *) Since the identity function has type 'a->'a, the natural thing is to treat the ref as being of type ('a->'a) ref: val r = ref (fn x => x) (* r: ('a->'a) ref *) Now when we use a polymorphic identifier, like r, we instantiate it to each particular use. E.g. the following is legal by our current rules: r := (fn b => if b then false else true) (* r : ('b->'b) ref fn..: (bool->bool) := : 'c ref * 'c -> 'c solution: 'c = 'b->'b, 'b = bool *) As is the following: (!r) 3 (* r : ('b->'b) ref 3 : int ! : 'c ref -> 'c solution: 'c = 'b->'b, 'b = int *) (* val it:int = ???? *) But even though this program all typechecks, the application of the bool function to the int argument breaks type safety, possibly crashing the system! Where did things go wrong? Clearly, we can't allow a reference to be polymorphic, and allow different instances to be used for stores and fetches. SML'90 had one fix, introducing weak type variables for refs and similar things, that prevented them from being fully polymorphic. Pretty complicated. SML'97 changed things by introducing the value restriction: only "syntactic values" can be given polymorphic types (when let-bound to identifiers). Syntactic values include constants, lambda expressions, identifier references, and datatype constructors (including cons) applied to syntactic values, but they exclude function applications, which includes the ref value constructor (ref is treated as a function that returns a new reference cell, not a data constructor). So in SML'97 (and SML'90), the original ref creation expression is flagged as in error: val r = ref (fn x => x) (* error: non-generalizable type variable.... *) The "non-generalizable type variable" stuff is SML's way of saying that there were free type variables that would have been turned into a polymorphic type but couldn't be because the right-hand-side of the binding isn't a syntactic value.