We introduced Ivars, which are Deferreds that can be determined by the program
We implemented bind in terms of Ivars and upon
We implemented either in terms of Ivars and upon
Note: many of the functions you want to build using Ivars are already available; be sure to consult the Deferred
module first!
An Ivar is a Deferred that the program can determine. As with Deferreds, once an Ivar is filled, its value cannot be changed. The Ivar
module contains the following functions:
module Ivar : sig
type 'a t
(** creates an empty Ivar *)
val create : unit -> 'a Ivar.t
(** fills the Ivar if it is empty. Raises an exception if it is already full *)
val fill : 'a Ivar.t -> 'a -> unit
val is_full : 'a Ivar.t -> bool
val is_empty : 'a Ivar.t -> bool
(** Asynchronously read the value of an Ivar. result becomes determined when
the Ivar is filled *)
val read : 'a Ivar.t -> 'a Deferred.t
end
The read
function converts an Ivar.t
into a Deferred.t
. In fact, Ivar
s and Deferred
s are defined internally as being the same, but the two different interfaces allow the ability to write to a deferred to be encapsulated. For example, we could imagine having bind
return an Ivar.t
instead of a Deferred.t
, but this means that the caller to bind can misuse the value that gets returned by determining it before it should.
The fill
operation enforces the write-once constraint on deferred values; calling fill
on an Ivar
that is already full raises an exception.
As a first exercise, we implemented bind
using upon
:
let bind d f =
let result = Ivar.create () in
upon d (fun x ->
upon (f x) (fun y -> Ivar.fill result y)
);
Ivar.read result
This matches the description of bind
given in a previous lecture: we first create a new "box" (result), the schedule f
to be run when d
is determined. We also schedule a function to be run when f
's result is determined; this function fills the result
box. After scheduling these functions to run, bind
Suppose we wanted to createa deferred that becomes determined when either of two deferreds becomes determined. We can use Ivar
s to do this:
type ('a,'b) choice = Left of 'a | Right of 'b
let either d1 d2 =
let result = Ivar.create () in
upon d1 (fun x ->
if Ivar.is_empty result
then Ivar.fill result x
else ()
);
upon d2 (fun y ->
if Ivar.is_empty result
then Ivar.fill result (Right y)
else ()
);
Ivar.read result
This function creates a new Ivar, and registers a callback with each of d1
and d2
that fills the result Ivar.
Two points that often cause confusion deserve mention. The first is that we don't have to worry about result becoming filled between the call to Ivar.is_empty
and Ivar.fill
. This is because while our code is running, the scheduler cannot preempt us and cause anything else to run.
The second point is that we think of either
as returning the value of the "first" of d1
and d2
that becomes determined. This is more or less true, but if d1
and d2
become scheduled at nearly the same time, the scheduler may cause our callbacks to execute in a different order. We aren't allowed to reason about timing; our program cannot really determine which of two things happens first.
Note that this is a good way of thinking for advanced systems that are distributed across a network. Because of networking delays, dropped packets, poorly synchronized clocks, and other features of distributed programming, it is important not to try to reason about time when building distributed systems.
As an exercise, try implementing either
without using Ivar
s. You will find that you cannot.