Rhope Blog

Blog Home Downloads Documentation RSS

Getting Started with Rhope: Semantics

by Mike Pavone

This post is going to give an overview of the major semantic differences between Rhope and most mainstream languages. It will focus mainly on differences related to concurrency but it won't be limited exclusively to those topics.

One of the biggest semantic differences between Rhope and most other languages is that all operations automatically operate in parallel. Take a look at the following code:

Do Something[]
Do Something Else[]

In the above snippet, Do Something and Do Something Else would execute simultaneously. Of course, not all code can be run in parallel. If one operation needs to work on data produced by another operation, those operations need to be executed serially.

foo <- Do Something[]
Do Something Else[foo]

In the above example Do Something Else won't execute until Do Something is done. More generally, a call to a worker isn't performed until all of its inputs have been provided with data. The order the statements doesn't have an effect on execution order. The above example could have had its lines arranged in the reverse order and its behavior would have been the same. This behavior has an interesting side effect. Basic flow control structures like If, can be normal Workers that selectively output data.

foo,bar <- If[some expression]
Print[foo]
Print[bar]

In the above example, if "some expression" evaluates to Yes the variable foo will be set to Yes and bar will not be populated. If "some expression" evaluates to No the variable bar will be set to No and foo will be left unpopulated. In either case, only one of the two calls to Print will be executed. Since the above can be a somewhat awkward way to do flow control, Rhope has a special syntax to make things a bit easier.

If[some expression]
|:
Print[~]
:||:
Print[~]
:|

The above code will do exactly the same thing as the example before it. Inside of a block (excluding the outer most block that defines the boundaries of a worker definition), ~ refers to an output of the expression. In the first block it refers to the first output, in the second block it refers to the second and so on. This can be done with any expression, not just If. The following code prints out the sum of 2 and 3:

[2] + [3]
|:
Print[~]
:|

Going back to the If example, you might be wondering how you would conditionally execute code if you didn't want to pass the result of If to the worker you wanted to execute. Rhope's block syntax has a little extra magic to deal with this. If you place a call to a worker inside a block, but don't use ~ as one of the inputs it will still create a link between the output corresponding with that block and the worker, but no data will be passed. Here's an example:

If[some expression]
|:
Print["This will be printed if the expression is Yes"]
:||:
Print["This will be printed if the expression is No"]
:|

This same mechanism can also be used to serialize IO actions:

Print["I will get printed first"]
|: Print["I will get printed second"] :|

Another major difference between Rhope and most mainstream languages is that most operations in the language have pass by value semantics. What this means is that instead of modifying an object they actually create a new object with the requested changes. While this can be less efficient, it's part of the magic juice that makes concurrent programming in Rhope so easy. You generally don't have to worry about some other piece of coding running in parallel changing the data while you're working on it. For those of you, worried about the performance of such behavior, fear not. Internally, all values are actually passed by reference. A copy is only made when an operation wants to modify a value and the reference count for the value is greater than 1.

The last semantic difference I'm going to cover has to do with global variables. In languages that use a thread model for concurrency, working with global variables can be quite a bear. You need to protect them with some kind of mutually exclusive lock and if you forget to do so somewhere you'll have a rather difficult to track down bug on your hands. Rhope borrows the concept of transactions from databases to make concurrent access to global variables a breeze. In Rhope, global variables are split into global stores which are sort of like namespaces in other languages. When a worker that has been declared to use a particular global store starts, it creates a new transaction. When the worker exits, the changes it made are committed to the store. If the store has changed between when the transaction was started and when the commit happens, the commit fails and the worker is automatically run again. Here's an example of a worker that increments a variable in a global store named foo:

Increment(0,0) uses foo
|:
::Counter <- [::Counter] + [1]
:|

That's it. The above example can be safely called multiple times in parallel with no extra work.

Username: Password:
Don't have an account? Register now!