Messages - Spicery/Nutmeg GitHub Wiki

Nutmeg provides Actor-style asynchronous message-passing. Messages can be thought of as deferred method-calls that are queued on a per-receiver basis. The Nutmeg runtime supports multiple threads whose activities are cleanly isolated, thanks to static checking.

Const objects are freely shared across all threads. All other (non-const) objects are owned or 'locked' to the thread they were created in. They can be passed across thread boundaries but they are manifested as handles in other threads i.e. the other threads do not have direct access to the objects.

Thread Creation

Threads are explicitly created by instantiating [capsules] using the NewTask procedure. This returns a handle to the newly created capsule.

my_handle := MyCapsule.NewTask()

Capsules keep state nicely contained and their properties are automatically enforced by the Nutmeg compiler.

Typical Message Sending

A typical message-send looks like this, where myobj is an object, f is a method that plays the role of a message, and x and y are arguments to be passed to f:

(a, b) := send myobj.f( x, y )                   # PATTERN := send DOT_APPLY
send myobj.f( x, y );;                           # No results returned
send myobj.f( x, y )                             # EXPR must return a single value

The thread that is used depends on the nature of the object myobj and the method f.

  • If myobj is a handle then it will run on the owning thread of that object
  • If the method is a function or finesse then it is eligible to be run on any thread (which will be taken from the thread pool)
  • Otherwise the computation is deferred i.e. run in the same thread when the results are implicitly forced

Note that the return values from a message are always generated immediately, they are futures or (equivalently) deferred computations. Strictly speaking these are implicit futures because they can be treated exactly like the values they deliver. However, when those return values are required, the current thread may block until they are available.

Note that the result from a send will apparently bind to all possible patterns! This is to avoid blocking if the number of results is unknown. So both of these are possible - at least temporarily:

a := send x.msg()
(x, y) := send x.msg()

However, when the actual return values are available, the delayed binding occurs and an error will be raised if any of the patterns do not match. A Nutmeg implementation is encouraged to warn of invalid pattern matches as early as possible, ideally at compile-time.

General mechanism

In general, you may run any expression in any thread. Given a handle my_handle you simply use a slightly different version of send. Just as with the compact version of send there are three usages:

( a, b ) := send my_handle: STATEMENTS end    # Results must bind to the PATTERN when available
send my_handle: STATEMENTS end;;              # Results are discarded (fire & forget)
send my_handle: STATEMENTS end                # A single result is required

See Also

Closely related to send is the notion of deferral. These are always delayed until their results are required. If the compiler can determine that the expression is safe to run on another thread it may do, where safety takes account of eager vs lazy evaluation.

(a, b) := |EXPR|                                 # Defers a pattern match to a deferred expression EXPR
|EXPR|                                           # Defers a single-valued expression EXPR