References

Flix supports mutable scoped references. A reference is a box whose value can change over time. The three key reference operations are:

  • Creating a new reference Ref.fresh(rc, e).
  • Dereferencing a reference Ref.get(e).
  • Assigning to a reference Ref.put(e, e).

In Flix, the type of a reference is Ref[t, r] where t is the type of the element and r is its region. Like all mutable memory in Flix, every reference must belong to some region. Reading from and writing to a reference are effectful operations. For example, reading the value of a reference Ref[t, r] has effect r.

The Ref.fresh(rc, e) operation allocates a reference cell in a region of the heap and returns its location, the Ref.get operation dereferences a location and returns the content of a reference cell, and the assignment Ref.put operation changes the value of a reference cell. Informally, a reference cell can be thought of as an "object" with a single field that can be changed.

Allocating References

A reference cell is allocated with the Ref.fresh(rc, e) function. For example:

region rc {
    let c = Ref.fresh(rc, 42);
    println(Ref.get(c))
}

Here we introduce a region named rc. Inside the region, we create a reference cell called c with the value 42 which we then dereference and print.

Dereferencing References

A reference cell is accessed (dereferenced) with the Ref.get function. For example:

region rc {
    let c = Ref.fresh(rc, 42);
    let x = Ref.get(c);
    let y = Ref.get(c);
    println(x + y)
}

Here the program prints 42 + 42 = 84.

Assignment

We can update the value of a reference cell. For example:

region rc {
    let c = Ref.fresh(rc, 0);
    Ref.put(Ref.get(c) + 1, c);
    Ref.put(Ref.get(c) + 1, c);
    Ref.put(Ref.get(c) + 1, c);
    println(Ref.get(c))
}

Here the program creates a reference cell c with the value 0. We dereference the cell and increment its value three times. Hence the program prints 3.

Example: A Simple Counter

We can use references to implement a simple counter:

enum Counter[r: Region] { // The Region here is a type-kind
    case Counter(Ref[Int32, r])
}

def newCounter(rc: Region[r]): Counter[r] \ r = Counter.Counter(Ref.fresh(rc, 0))

def getCount(c: Counter[r]): Int32 \ r =
    let Counter.Counter(l) = c;
    Ref.get(l)

def increment(c: Counter[r]): Unit \ r =
    let Counter.Counter(l) = c;
    Ref.put(Ref.get(l) + 1, l)

def main(): Unit \ IO =
    region rc {
        let c = newCounter(rc);
        increment(c);
        increment(c);
        increment(c);
        getCount(c) |> println
    }

Here the Counter data type has a region type parameter. This is required since the counter internally uses a reference that requires a region. Hence Counters are also scoped. Note that the newCounter function requires a region handle to create a new Counter. Moreover, note that the functions getCount and increment both have the r effect.

Aliasing and References to References

References naturally support aliasing since that is their purpose. For example:

region rc {
    let l1 = Ref.fresh(rc, 42);
    let l2 = l1;
    Ref.put(84, l2);
    println(Ref.get(l1))
}

Prints 84 because the reference cell that l1 points to is modified through the alias l2.

References can also point to references as the following example illustrates:

region rc {
    let l1 = Ref.fresh(rc, 42);
    let l2 = Ref.fresh(rc, l1);
    let rs = Ref.get(Ref.get(l2));
    println(rs)
}

Here the type of l2 is Ref[Ref[Int32, rc], rc].

Mutable Tuples and Records

Flix tuples and records are immutable. However, tuples and records may contain mutable references.

For example, here is a pair that contains two mutable references:

region rc {
    let p = (Ref.fresh(rc, 1), Ref.fresh(rc, 2));
    Ref.put(123, fst(p))
};

The type of the pair is (Ref[Int32, rc], Ref[Int32, rc]). The assignment does not change the pair but instead changes the value of the reference cell in the first component.

Similarly, here is a record that contains two mutable references:

region rc {
    let r = { fstName = Ref.fresh(rc, "Lucky"), lstName = Ref.fresh(rc, "Luke") };
    Ref.put("Unlucky", r.fstName)
};

The type of the record is { fstName = Ref[String, rc], lstName = Ref[String, rc] }. Again, the assignment does not change the record, but instead changes the value of the reference cell corresponding to the fstName label.