Lecture 19: Ivars

Summary

Ivars

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, Ivars and Deferreds 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.

Implementing bind

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

Immplementing either

Suppose we wanted to createa deferred that becomes determined when either of two deferreds becomes determined. We can use Ivars 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 Ivars. You will find that you cannot.