4.4. Mutable References

Variables in Orc are immutable. There is no assignment operator, and there is no way to change the value of a bound variable. However, it is often useful to have mutable state when writing certain algorithms. The Orc library contains two sites that offer simple mutable storage: Ref and Cell. It also provides the site Array to create mutable arrays.

A word of caution: References, cells, and other mutable objects may be accessed concurrently by many different parts of an Orc program, so race conditions may arise.

4.4.1. Rewritable references

The Ref site creates rewritable reference cells.

val r = Ref(0)
Println(r.read()) >>
r.write(2) >> 
Println(r.read()) >>
stop

{-
OUTPUT:
0
2
-}

These are very similar to ML's ref cells. r.write(v) stores the value v in the reference r, overwriting any previous value, and publishes a signal. r.read() publishes the current value stored in r.

However, unlike in ML, a reference cell can be left initially empty by calling Ref with no arguments. A read operation on an empty cell blocks until the cell is written.

{- Create a cell, and wait 1 second before initializing it.
   The read operation blocks until the write occurs. 
-}

val r = Ref()
r.read() | Rwait(1000) >> r.write(1) >> stop

{-
OUTPUT:
1
-}

4.4.2. Write-once references

The Orc library also offers write-once reference cells, using the Cell site. A write-once cell has no initial value. Read operations block until the cell has been written. A write operation succeeds only if the cell is empty; subsequent write operations simply halt.

{- Create a cell, try to write to it twice, and read it.
   The read will block until a write occurs
   and only one write will succeed. 
-}

val r = Cell()
  Rwait(1000) >> r.write(2) >> Println("Wrote 2") >> stop
| Rwait(2000) >> r.write(3) >> Println("Wrote 3") >> stop
| r.read()

{-
OUTPUT:PERMUTABLE
2
Wrote 2
-}

Write-once cells are very useful for concurrent programming, and they are often safer than rewritable reference cells, since the value cannot be changed once it has been written. The use of write-once cells for concurrent programming is not a new idea; they have been studied extensively in the context of the Oz programming language.

4.4.3. Syntax for manipulating references

Orc provides syntactic sugar for reading and writing mutable storage:

  • x? is equivalent to x.read(). This operator is of equal precedence with the dot operator and function application, so you can write things like x.y?.v?. This operator is very similar to the C languages's * operator, but is postfix instead of prefix.
  • x := y is equivalent to x.write(y). This operator has higher precedence than the concurrency combinators and if/then/else, but lower precedence than any of the other operators.

Here is a previous example rewritten using this syntactic sugar:

{- Create a cell, try to write to it twice, and read it.
   The read will block until a write occurs
   and only one write will succeed.
-}

val r = Cell()
  Rwait(1000) >> r := 2 >> Println("Wrote 2") >> stop
| Rwait(2000) >> r := 3 >> Println("Wrote 3") >> stop
| r?

{-
OUTPUT:PERMUTABLE
2
Wrote 2
-}

4.4.4. Arrays

While lists are a very useful data structure, they are not mutable, and they are not indexed. However, these properties are often needed in practice, so the Orc standard library provides a function Array to create mutable arrays.

Array(n) creates an array of size n whose elements are all initially null. The array is used like a function; the call A(i) returns the ith element of the array A, which is then treated as a reference, just like the references created by Ref. A call with an out-of-bounds index halts, possibly reporting an error.

The following program creates an array of size 10, and initializes each index i with the ith power of 2. It then reads the array values at indices 3, 6, and 10. The read at index 10 halts because it is out of bounds (arrays are indexed from 0).

{- Create and initialize an array, then halt on out of bounds read -}
val a = Array(10)
def initialize(i) = 
  if (i <: 10) 
    then a(i) := 2 ** i  >>  initialize(i+1)
    else signal
initialize(0) >> (a(3)? | a(6)? | a(10)?)

{-
OUTPUT:PERMUTABLE
8
64
Error: java.lang.ArrayIndexOutOfBoundsException: 10
-}

The standard library also provides a helper function fillArray which makes array initialization easier. fillArray(a, f) initializes array a using function f by setting element a(i) to the first value published by f(i). When the array is fully initialized, fillArray returns the array a that was passed (which makes it easier to simultaneously create and initialize an array). Here are a few examples:

{- Create an array of 10 elements; element i is the ith power of 2 -}
fillArray(Array(10), lambda(i) = 2 ** i) >a>
a(4)?

{-
OUTPUT:
16
-}

{- Create an array of 5 elements; each element is a newly created channel -}
fillArray(Array(5), lambda(_) = Channel())

{- Create an array of 2 channels -}
val A = fillArray(Array(2), lambda(_) = Channel())

{- Send true on channel 0,
   listen for a value on channel 0 and forward it to channel 1, 
   and listen for a value on channel 1 and publish it. 
-}

  A(0)?.put(true) >> stop
| A(0)?.get() >x> A(1)?.put(x) >> stop
| A(1)?.get()

{-
OUTPUT:
true
-}

Since arrays are accessed by index, there is a library function specifically designed to make programming with indices easier. The function upto(n) publishes all of the numbers from 0 to n-1 simultaneously; thus, it is very easy to access all of the elements of an array simultaneously. Suppose we have an array A of n email addresses and would like to send the message m to each one.

upto(n) >i> A(i)? >address> Email(address, m)