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.
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 -}
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.
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 -}
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 i
th 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)