7.1. Refs#
A ref is like a pointer or reference in an imperative language. It is a location in memory whose contents may change. Refs are also called ref cells, the idea being that there’s a cell in memory that can change.
Here’s an example of creating a ref, getting the value from inside it, changing its contents, and observing the changed contents:
let x = ref 0;;
val x : int ref = {contents = 0}
!x;;
- : int = 0
x := 1;;
- : unit = ()
!x;;
- : int = 1
The first phrase, let x = ref 0
, creates a reference using the ref
keyword.
That’s a location in memory whose contents are initialized to 0
. Think of the
location itself as being an address—for example, 0x3110bae0—even
though there’s no way to write down such an address in an OCaml program. The
keyword ref
is what causes the memory location to be allocated and
initialized.
The first part of the response from OCaml, val x : int ref
, indicates that x
is a variable whose type is int ref
. We have a new type constructor here. Much
like list
and option
are type constructors, so is ref
. A t ref
, for any
type t
, is a reference to a memory location that is guaranteed to contain a
value of type t
. As usual, we should read a type from right to left: t ref
means a reference to a t
. The second part of the response shows us the
contents of the memory location. Indeed, the contents have been initialized to
0
.
The second phrase, !x
, dereferences x
and returns the contents of the memory
location. Note that !
is the dereference operator in OCaml, not Boolean
negation.
The third phrase, x := 1
, is an assignment. It mutates the contents x
to be
1
. Note that x
itself still points to the same location (i.e., address) in
memory. Memory is mutable; variable bindings are not. What changes is the
contents. The response from OCaml is simply ()
, meaning that the assignment
took place—much like printing functions return ()
to indicate that the
printing did happen.
The fourth phrase, !x
again dereferences x
to demonstrate that the contents
of the memory location did indeed change.
7.1.1. Aliasing#
Now that we have refs, we have aliasing: two refs could point to the same memory location, hence updating through one causes the other to also be updated. For example,
let x = ref 42;;
let y = ref 42;;
let z = x;;
x := 43;;
let w = !y + !z;;
val x : int ref = {contents = 42}
val y : int ref = {contents = 42}
val z : int ref = {contents = 42}
- : unit = ()
val w : int = 85
The result of executing that code is that w
is bound to 85
, because
let z = x
causes z
and x
to become aliases, hence updating x
to be 43
also causes z
to be 43
.
7.1.2. Syntax and Semantics#
The semantics of refs is based on locations in memory. Locations are values that can be passed to and returned from functions. But unlike other values (e.g., integers, variants), there is no way to directly write a location in an OCaml program. That’s different than languages like C, in which programmers can directly write memory addresses and do arithmetic on pointers. C programmers want that kind of low-level access to do things like interfacing with hardware and building operating systems. Higher-level programmers are willing to forego it to get memory safety. That’s a hard term to define, but according to Hicks 2014 it intuitively means that
pointers are only created in a safe way that defines their legal memory region,
pointers can only be dereferenced if they point to their allotted memory region,
that region is (still) defined.
Syntax.
Ref creation:
ref e
Ref assignment:
e1 := e2
Dereference:
!e
Dynamic semantics.
To evaluate
ref e
,Evaluate
e
to a valuev
Allocate a new location
loc
in memory to holdv
Store
v
inloc
Return
loc
To evaluate
e1 := e2
,Evaluate
e2
to a valuev
, ande1
to a locationloc
.Store
v
inloc
.Return
()
, i.e., unit.
To evaluate
!e
,Evaluate
e
to a locationloc
.Return the contents of
loc
.
Static semantics.
We have a new type constructor, ref
, such that t ref
is a type for any type
t
. Note that the ref
keyword is used in two ways: as a type constructor, and
as an expression that constructs refs.
ref e : t ref
ife : t
.e1 := e2 : unit
ife1 : t ref
ande2 : t
.!e : t
ife : t ref
.
7.1.3. Sequencing of Effects#
The semicolon operator is used to sequence effects, such as mutating refs. We’ve seen semicolon occur previously with printing. Now that we’re studying mutability, it’s time to treat it formally.
Syntax:
e1; e2
Dynamic semantics: To evaluate
e1; e2
,First evaluate
e1
to a valuev1
.Then evaluate
e2
to a valuev2
.Return
v2
. (v1
is not used at all.)If there are multiple expressions in a sequence, e.g.,
e1; e2; ...; en
, then evaluate each one in order from left to right, returning onlyvn
.
Static semantics:
e1; e2 : t
ife1 : unit
ande2 : t
. Similarly,e1; e2; ...; en : t
ife1 : unit
,e2 : unit
, … (i.e., all expressions excepten
have typeunit
), anden : t
.
The typing rule for semicolon is designed to prevent programmer mistakes. For
example, a programmer who writes 2+3; 7
probably didn’t mean to: there’s no
reason to evaluate 2+3
then throw away the result and instead return 7
. The
compiler will give you a warning if you violate this particular typing rule.
To get rid of the warning (if you’re sure that’s what you need to do), there’s a
function ignore : 'a -> unit
in the standard library. Using it,
ignore(2+3); 7
will compile without a warning. Of course, you could code up
ignore
yourself: let ignore _ = ()
.
7.1.4. Example: Mutable Counter#
Here is code that implements a counter. Every time next_val
is called, it
returns one more than the previous time.
let counter = ref 0
let next_val =
fun () ->
counter := !counter + 1;
!counter
val counter : int ref = {contents = 0}
val next_val : unit -> int = <fun>
next_val ()
- : int = 1
next_val ()
- : int = 2
next_val ()
- : int = 3
In the implementation of next_val
, there are two expressions separated by
semicolon. The first expression, counter := !counter + 1
, is an assignment
that increments counter
by 1. The second expression, !counter
, returns the
newly incremented contents of counter
.
The next_val
function is unusual in that every time we call it, it returns a
different value. That’s quite different than any of the functions we’ve
implemented ourselves so far, which have always been deterministic: for a
given input, they always produced the same output. On the other hand, some
functions are nondeterministic: each invocation of the function might produce
a different output despite receiving the same input. In the standard library,
for example, functions in the Random
module are nondeterministic, as is
Stdlib.read_line
, which reads input from the user. It’s no coincidence that
those happen to be implemented using mutable features.
We could improve our counter in a couple ways. First, there is a library
function incr : int ref -> unit
that increments an int ref
by 1. Thus it is
like the ++
operator that is familiar from many languages in the C family.
Using it, we could write incr counter
instead of counter := !counter + 1
.
(There’s also a decr
function that decrements by 1.)
Second, the way we coded the counter currently exposes the counter
variable to
the outside world. Maybe we’d prefer to hide it so that clients of next_val
can’t directly change it. We could do so by nesting counter
inside the scope
of next_val
:
let next_val =
let counter = ref 0 in
fun () ->
incr counter;
!counter
val next_val : unit -> int = <fun>
Now counter
is in scope inside of next_val
, but not accessible outside that
scope.
When we gave the dynamic semantics of let expressions before, we talked about
substitution. One way to think about the definition of next_val
is as follows.
First, the expression
ref 0
is evaluated. That returns a locationloc
, which is an address in memory. The contents of that address are initialized to0
.Second, everywhere in the body of the let expression that
counter
occurs, we substitute for it that location. So we get:fun () -> incr loc; !loc
Third, that anonymous function is bound to
next_val
.
So any time next_val
is called, it increments and returns the contents of that
one memory location loc
.
Now imagine that we instead had written the following (broken) code:
let next_val_broken = fun () ->
let counter = ref 0 in
incr counter;
!counter
val next_val_broken : unit -> int = <fun>
It’s only a little different: the binding of counter
occurs after
the fun () ->
instead of before. But it makes a huge difference:
next_val_broken ();;
next_val_broken ();;
next_val_broken ();;
- : int = 1
- : int = 1
- : int = 1
Every time we call next_val_broken
, it returns 1
: we no longer have a
counter. What’s going wrong here?
The problem is that every time next_val_broken
is called, the first thing it
does is to evaluate ref 0
to a new location that is initialized to 0
. That
location is then incremented to 1
, and 1
is returned. Every call to
next_val_broken
is thus allocating a new ref cell, whereas next_val
allocates just one new ref cell.
7.1.5. Example: Pointers#
In languages like C, pointers combine two features: they can be null, and they can be changed. (Java has a similar construct with object references, but that term is confusing in our OCaml context since “reference” currently means a ref cell. So we’ll stick with the word “pointer”.) Let’s code up pointers using OCaml ref cells.
type 'a pointer = 'a ref option
type 'a pointer = 'a ref option
As usual, read that type right to left. The option
part of it encodes the fact
that a pointer might be null. We’re using None
to represent that possibility.
let null : 'a pointer = None
val null : 'a pointer = None
The ref
part of the type encodes the fact that the contents are mutable. We
can create a helper function to allocate and initialize the contents of a new
pointer:
let malloc (x : 'a) : 'a pointer = Some (ref x)
val malloc : 'a -> 'a pointer = <fun>
Now we could create a pointer to any value we like:
let p = malloc 42
val p : int pointer = Some {contents = 42}
Dereferencing a pointer is the *
prefix operator in C. It returns the
contents of the pointer, and raises an exception if the pointer is null:
exception Segfault
let deref (ptr : 'a pointer) : 'a =
match ptr with None -> raise Segfault | Some r -> !r
exception Segfault
val deref : 'a pointer -> 'a = <fun>
deref p
- : int = 42
deref null
Exception: Segfault.
Raised at deref in file "[17]", line 4, characters 25-39
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150
We could even introduce our own OCaml operator for dereference. We have to put
~
in front of it to make it parse as a prefix operator, though.
let ( ~* ) = deref;;
~*p
val ( ~* ) : 'a pointer -> 'a = <fun>
- : int = 42
In C, an assignment through a pointer is written *p = x
. That changes
the memory to which p
points, making it contain x
. We can code up
that operator as follows:
let assign (ptr : 'a pointer) (x : 'a) : unit =
match ptr with None -> raise Segfault | Some r -> r := x
val assign : 'a pointer -> 'a -> unit = <fun>
assign p 2;
deref p
- : int = 2
assign null 0
Exception: Segfault.
Raised at assign in file "[21]", line 2, characters 25-39
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150
Again, we could introduce our own OCaml operator for that, though it’s hard to
pick a good symbol involving *
and =
that won’t be misunderstood as
involving multiplication:
let ( =* ) = assign;;
p =* 3;;
~*p
val ( =* ) : 'a pointer -> 'a -> unit = <fun>
- : unit = ()
- : int = 3
The one thing we can’t do is treat a pointer as an integer. C allows that, including taking the address of a variable, which enables pointer arithmetic. That’s great for efficiency, but also terrible because it leads to all kinds of program errors and security vulnerabilities.
Evil Secret
Okay that wasn’t actually true what we just said, but this is dangerous
knowledge that you really shouldn’t even read. There is an undocumented
function Obj.magic
that we could use to get a memory address of a ref:
let address (ptr : 'a pointer) : int =
match ptr with None -> 0 | Some r -> Obj.magic r
let ( ~& ) = address
But you have to promise to never, ever use that function yourself, because it completely circumvents the safety of the OCaml type system. All bets are off if you do.
None of this pointer encoding is part of the OCaml standard library, because you don’t need it. You can always use refs and options yourself as you need to. Coding as we just did above is not particularly idiomatic. The reason we did it was to illustrate the relationship between OCaml refs and C pointers (equivalently, Java references).
7.1.6. Example: Recursion Without Rec#
Here’s a neat trick that’s possible with refs: we can build recursive functions
without ever using the keyword rec
. Suppose we want to define a recursive
function such as fact
, which we would normally write as follows:
let rec fact_rec n = if n = 0 then 1 else n * fact_rec (n - 1)
val fact_rec : int -> int = <fun>
We want to define that function without using rec
. We can begin by
defining a ref to an obviously incorrect version of the function:
let fact0 = ref (fun x -> x + 0)
val fact0 : (int -> int) ref = {contents = <fun>}
The way in which fact0
is incorrect is actually irrelevant. We just need it to
have the right type. We could just as well have used fun x -> x
instead of
fun x -> x + 0
.
At this point, fact0
clearly doesn’t compute the factorial function.
For example, \(5!\) ought to be 120, but that’s not what fact0
computes:
!fact0 5
- : int = 5
Next, we write fact
as usual, but without rec
. At the place where we need to
make the recursive call, we instead invoke the function stored inside fact0
:
let fact n = if n = 0 then 1 else n * !fact0 (n - 1)
val fact : int -> int = <fun>
Now fact
does actually get the right answer for 0
, but not for 5
:
fact 0;;
fact 5;;
- : int = 1
- : int = 20
The reason it’s not right for 5
is that the recursive call isn’t actually
to the right function. We want the recursive call to go to fact
, not to
fact0
. So here’s the trick: we mutate fact0
to point to fact
:
fact0 := fact
- : unit = ()
Now when fact
makes its recursive call and dereferences fact0
, it gets
back itself! That makes the computation correct:
fact 5
- : int = 120
Abstracting a little, here’s what we did. We started with a function that is recursive:
let rec f x = ... f y ...
We rewrote it as follows:
let f0 = ref (fun x -> x)
let f x = ... !f0 y ...
f0 := f
Now f
will compute the same result as it did in the version where we defined
it with rec
.
What’s happening here is sometimes called “tying the recursive knot”: we update
the reference to f0
to point to f
, such that when f
dereferences f0
, it
gets itself back. The initial function to which we made f0
point (in this case
the identity function) doesn’t really matter; it’s just there as a placeholder
until we tie the knot.
7.1.7. Weak Type Variables#
Perhaps you have already tried using the identity function to define fact0
,
as we mentioned above. If so, you will have encountered this rather puzzling
output:
let fact0 = ref (fun x -> x)
val fact0 : ('_weak1 -> '_weak1) ref = {contents = <fun>}
What is this strange type for the identity function, '_weak1 -> '_weak1
? Why
isn’t it the usual 'a -> 'a
?
The answer has to do with a particularly tricky interaction between polymorphism
and mutability. In a later chapter on interpreters, we’ll learn how type
inference works, and at that point we’ll be able to explain the problem in
detail. In short, allowing the type 'a -> 'a
for that ref would lead to the
possibility of programs that crash at run time because of type errors.
For now, think about it this way: although the value stored in a ref cell is
permitted to change, the type of that value is not. And if OCaml gave
ref (fun x -> x)
the type ('a -> 'a) ref
, then that cell could first store
fun x -> x + 1 : int -> int
but later store
fun x -> s ^ "!" : string -> string
. That would be the kind of change in type
that is not allowed.
So OCaml uses weak type variables to stand for unknown but not polymorphic
types. These variables always start with _weak
. Essentially, type inference
for these is just not finished yet. Once you give OCaml enough information, it
will finish type inference and replace the weak type variable with the actual
type:
!fact0
- : '_weak1 -> '_weak1 = <fun>
!fact0 1
- : int = 1
!fact0
- : int -> int = <fun>
After the application of !fact0
to 1
, OCaml now knows that the function
is meant to have type int -> int
. So from then on, that’s the only type
at which it can be used. It can’t, for example, be applied to a string.
!fact0 "camel"
File "[36]", line 1, characters 7-14:
1 | !fact0 "camel"
^^^^^^^
Error: This expression has type string but an expression was expected of type
int
If you would like to learn more about weak type variables right now, take a look at Section 2 of Relaxing the value restriction by Jacques Garrigue, or this section of the OCaml manual.
7.1.8. Physical Equality#
OCaml has two equality operators, physical equality and structural equality. The
documentation of Stdlib.(==)
explains physical equality:
e1 == e2
tests for physical equality ofe1
ande2
. On mutable types such as references, arrays, byte sequences, records with mutable fields and objects with mutable instance variables,e1 == e2
istrue
if and only if physical modification ofe1
also affectse2
. On non-mutable types, the behavior of( == )
is implementation-dependent; however, it is guaranteed thate1 == e2
impliescompare e1 e2 = 0
.
One interpretation could be that ==
should be used only when comparing refs
(and other mutable data types) to see whether they point to the same location in
memory. Otherwise, don’t use ==
.
Structural equality is also explained in the documentation of Stdlib.(=)
:
e1 = e2
tests for structural equality ofe1
ande2
. Mutable structures (e.g. references and arrays) are equal if and only if their current contents are structurally equal, even if the two mutable objects are not the same physical object. Equality between functional values raisesInvalid_argument
. Equality between cyclic data structures may not terminate.
Structural equality is usually what you want to test. For refs, it checks whether the contents of the memory location are equal, regardless of whether they are the same location.
The negation of physical equality is !=
, and the negation of structural
equality is <>
. This can be hard to remember.
Here are some examples involving equality and refs to illustrate the difference
between structural equality (=
) and physical equality (==
):
let r1 = ref 42
let r2 = ref 42
val r1 : int ref = {contents = 42}
val r2 : int ref = {contents = 42}
A ref is physically equal to itself, but not to another ref that is a different location in memory:
r1 == r1
- : bool = true
r1 == r2
- : bool = false
r1 != r2
- : bool = true
Two refs that are at different locations in memory but store structurally equal values are themselves structurally equal:
r1 = r1
- : bool = true
r1 = r2
- : bool = true
r1 <> r2
- : bool = false
Two refs that store structurally unequal values are themselves structurally unequal:
ref 42 <> ref 43
- : bool = true
7.1.9. Example: Singly-linked Lists#
OCaml’s built-in singly-linked lists are functional, not imperative. But we can code up imperative singly-linked lists, of course, with refs. (We could also use the pointers we invented above, but that only makes the code more complicated.)
We start by defining a type 'a node
for nodes of a list that contains values
of type 'a
. The next
field of a node is itself another list.
(** An ['a node] is a node of a mutable singly-linked list. It contains a value
of type ['a] and a link to the [next] node. *)
type 'a node = { next : 'a mlist; value : 'a }
(** An ['a mlist] is a mutable singly-linked list with elements of type ['a].
The [option] represents the possibility that the list is empty.
RI: The list does not contain any cycles. *)
and 'a mlist = 'a node option ref
type 'a node = { next : 'a mlist; value : 'a; }
and 'a mlist = 'a node option ref
To create an empty list, we simply return a ref to None
:
(** [empty ()] is an empty singly-linked list. *)
let empty () : 'a mlist = ref None
val empty : unit -> 'a mlist = <fun>
Note the type of empty
: instead of being a value, it is now a function. This
is typical of functions that create mutable data structures. At the end of this
section, we’ll return to why empty
has to be a function.
Inserting a new first element just requires creating a new node, linking from it to the original list, and mutating the list:
(** [insert_first lst v] mutates mlist [lst] by inserting value [v] as the
first value in the list. *)
let insert_first (lst : 'a mlist) (v : 'a) : unit =
lst := Some { next = ref !lst; value = v }
val insert_first : 'a mlist -> 'a -> unit = <fun>
Again, note the type of insert_first
. Rather than returning an 'a mlist
, it
returns unit
. This again is typical of functions that modify mutable data
structures.
In both empty
and insert_first
, the use of unit
makes the functions more
like their equivalents in an imperative language. The constructor for an empty
list in Java, for example, might not take any arguments (which is equivalent to
taking unit
). And the insert_first
operation for a Java linked list might
return void
, which is equivalent to returning unit
.
Finally, here’s a conversion function from our new mutable lists to OCaml’s built-in lists:
(** [to_list lst] is an OCaml list containing the same values as [lst]
in the same order. Not tail recursive. *)
let rec to_list (lst : 'a mlist) : 'a list =
match !lst with None -> [] | Some { next; value } -> value :: to_list next
val to_list : 'a mlist -> 'a list = <fun>
Now we can see mutability in action:
let lst0 = empty ();;
let lst1 = lst0;;
insert_first lst0 1;;
to_list lst1;;
val lst0 : '_weak2 mlist = {contents = None}
val lst1 : '_weak2 mlist = {contents = None}
- : unit = ()
- : int list = [1]
The change to lst0
mutates lst1
, because they are aliases.
The type of empty
. Returning to empty
, why must it be a function? It
might seem as though we could define it more simply as follows:
let empty = ref None
val empty : '_weak3 option ref = {contents = None}
But now there is only ever one ref that gets created, hence there is only one list ever in existence:
let lst2 = empty;;
let lst3 = empty;;
insert_first lst2 2;;
insert_first lst3 3;;
to_list lst2;;
to_list lst3;;
val lst2 : '_weak3 option ref = {contents = None}
val lst3 : '_weak3 option ref = {contents = None}
- : unit = ()
- : unit = ()
- : int list = [3; 2]
- : int list = [3; 2]
Note how the mutations affect both lists, because they are both aliases for the same ref.
By correctly making empty
a function, we guarantee that a new ref is
returned every time an empty list is created.
let empty () = ref None
val empty : unit -> 'a option ref = <fun>
It really doesn’t matter what argument that function takes, since it will never use it. We could define it as any of these in principle:
let empty _ = ref None
let empty (b : bool) = ref None
let empty (n : int) = ref None
(* etc. *)
val empty : 'a -> 'b option ref = <fun>
val empty : bool -> 'a option ref = <fun>
val empty : int -> 'a option ref = <fun>
But the reason we prefer unit
as the argument type is to indicate to the
client that the argument value is not going to be used. After all, there’s
nothing interesting that the function can do with the unit value. Another way to
think about that would be that a function whose input type is unit
is like a
function or method in an imperative language that takes in no arguments. For
example, in Java a linked list class could have a constructor that takes no
arguments and creates an empty list:
class LinkedList {
/** Returns an empty list. */
LinkedList() { ... }
}
Mutable values. In mlist
, the nodes of the list are mutable, but the
values are not. If we wanted the values also to be mutable, we can make
them refs too:
type 'a node = { next : 'a mlist; value : 'a ref }
and 'a mlist = 'a node option ref
let empty () : 'a mlist = ref None
let insert_first (lst : 'a mlist) (v : 'a) : unit =
lst := Some { next = ref !lst; value = ref v }
let rec set (lst : 'a mlist) (n : int) (v : 'a) : unit =
match (!lst, n) with
| None, _ -> invalid_arg "out of bounds"
| Some { value }, 0 -> value := v
| Some { next }, _ -> set next (n - 1) v
let rec to_list (lst : 'a mlist) : 'a list =
match !lst with None -> [] | Some { next; value } -> !value :: to_list next
Show code cell output
type 'a node = { next : 'a mlist; value : 'a ref; }
and 'a mlist = 'a node option ref
val empty : unit -> 'a mlist = <fun>
val insert_first : 'a mlist -> 'a -> unit = <fun>
val set : 'a mlist -> int -> 'a -> unit = <fun>
val to_list : 'a mlist -> 'a list = <fun>
Now rather than having to create new nodes if we want to change a value, we can directly mutate the value in a node:
let lst = empty ();;
insert_first lst 42;;
insert_first lst 41;;
to_list lst;;
set lst 1 43;;
to_list lst;;
val lst : '_weak4 mlist = {contents = None}
- : unit = ()
- : unit = ()
- : int list = [41; 42]
- : unit = ()
- : int list = [41; 43]