3 Goblins API
(require goblins) | package: goblins |
3.1 Actors
3.1.1 What is an "actor" in Goblins?
Goblins implements the actor model on top of Racket / scheme.1Sussman and Steele famously came to the conclusion that there was in fact no difference between actor-style message passing and procedure application in the lambda calculus, and indeed both are similar. However, there is a significant difference between synchronous call-and-return procedure application (which is what scheme implements in its most general form, and between actors in Goblins is handled by $) and asynchronous message passing (which in Goblins is handled with <-).
The fundamental operations of actors2Emphasis on "fundamental" operations. Extensions happen from here and vary widely between different systems that call themselves "actors". are:
An actor can send messages to other actors of which it has the address. (In Goblins, <-, possibly arguably $ as well.)
An actor may create new actors. (In Goblins, spawn.)
An actor may designate its behavior for the next message it handles. (In Goblins, this means returning a new message handler by wrapping that message handler in the actor’s bcom capability.)
Goblins’ extensions to these ideas are:
Both synchronous and asynchronous calls are defined and supported, between $ and <- respectively. However, $ is both convenient and important for systems that much be transactionally atomic (eg implementing money), it is limited to objects that are within the same vat. <- is more universal in that any actor on any vat may communicate with each other, but is asynchronous and cannot immediately return a resolved value.3For more on why this is, see chapters 13-15 of Mark Miller’s dissertation.. This was very influential on Goblins’ design, including the decision to move from a coroutine-centric approach to an E-style promise approach. It could be that coroutines are re-added, but would have to be done with extreme care; section 18.2 of that same thesis for an explaination of the challenges and a possible solution for introducing coroutines.
Raw message passing without a clear way to get values back can be painful. For this reason, <- implicitly returns a promise that can be resolved with on (and, for extra convenience and fewer round trips, supports Promise pipelining).4In this sense, <-np, which does not return a promise, is closer to the foundational actors message passing. The kind of value that promises give us can be constructed manually by providing return addresses to <-np, but this is painful given how common needing to operate on the result of an operation is.
Goblins composes with all the existing machinery of Racket/scheme, including normal procedure calls. Instead, Goblins builds its abstractions on top of it, but none of this needs to be thrown away.5Scheme is a beautiful language to build Goblins on top of, but actually we could build a Goblins-like abstraction layer on top of any programming language with sane lexical scoping and weak hash tables (so that actors which are no longer referenced can be garbage collected).
3.1.2 Constructors and bcom
A constructor is a procedure which builds the first message handler an actor will use to process messages / invocations. The constructor has one mandatory argument, traditionally called bcom (pronounced "become" or "bee-com") which can be used to set up a new message handler for future invocations.
> (require goblins) > (define am (make-actormap)) ; Outer procedure is the constructor. ; Implicitly takes a bcom argument. ; count argument may or may not be supplied by spawner.
> (define (^noisy-incrementer bcom [count 0]) ; Our message handler. (lambda ([increment-by 1]) (let ([new-count (+ count increment-by)]) ; Here we create a new version of ^noisy-incrementer ; with count scoped to a new incremented version. ; The second argument to bcom specifies a return value, ; would return void if unspecified. (bcom (^noisy-incrementer bcom new-count) (format "My new count is: ~a" new-count)))))
> (define incr1 (actormap-spawn! am ^noisy-incrementer)) > (actormap-poke! am incr1) "My new count is: 1"
> (actormap-poke! am incr1 20) "My new count is: 21"
> (define incr2 (actormap-spawn! am ^noisy-incrementer 18)) > (actormap-poke! am incr2 42) "My new count is: 60"
bcom, as shown above, is a capability (or technically a "sealer") to become another object. However, bcom does not apply a side effect; instead, it wraps the procedure and must be returned from the actor handler to set that to be its new message handler. Since this clobbers the space we would normally use to return a value (for whatever is waiting on the other end of a $ or a promise), bcom supports an optional second argument, which is that return value. If not provided, this defaults to (void).
1Sussman and Steele famously came to the conclusion that there was in fact no difference between actor-style message passing and procedure application in the lambda calculus, and indeed both are similar. However, there is a significant difference between synchronous call-and-return procedure application (which is what scheme implements in its most general form, and between actors in Goblins is handled by $) and asynchronous message passing (which in Goblins is handled with <-).
2Emphasis on "fundamental" operations. Extensions happen from here and vary widely between different systems that call themselves "actors".
3For more on why this is, see chapters 13-15 of Mark Miller’s dissertation.. This was very influential on Goblins’ design, including the decision to move from a coroutine-centric approach to an E-style promise approach. It could be that coroutines are re-added, but would have to be done with extreme care; section 18.2 of that same thesis for an explaination of the challenges and a possible solution for introducing coroutines.
4In this sense, <-np, which does not return a promise, is closer to the foundational actors message passing. The kind of value that promises give us can be constructed manually by providing return addresses to <-np, but this is painful given how common needing to operate on the result of an operation is.
5Scheme is a beautiful language to build Goblins on top of, but actually we could build a Goblins-like abstraction layer on top of any programming language with sane lexical scoping and weak hash tables (so that actors which are no longer referenced can be garbage collected).
3.2 Core procedures
The following procedures are the core API used to write Goblins code. All of them must be run within an "actor context", which is to say either from an actor running within a vat or an actormap or within one of the procedures used to bootstrap the vat/actormap.
procedure
(spawn constructor argument ...) → live-refr?
constructor : procedure? argument : any/c
The constructor is, as the name sounds, a goblins constructor, and is first passed a bcom argument (which is a way specify how it will behave on its next invocation) and then is passed the remaining arguments.
procedure
actor-refr : near-refr? arg : any/c
Provide a synchronous call against the current message handler of actor-refr, which must be a near live-refr? in the same vat as the currently running actor context. The value returned is that which is returned by the actor-refr’s message handler upon invocation (or, if actor-ref chose to bcom something else, the second argument passed to its bcom, or void if not provided.) Exceptions raised by actor-refr’s invocation will be propagated and can be captured.
Note that excape continuations can be set up between a caller of $ and can be used by the callee. However, a continuation barrier is installed and so capturing of the continuation is not possible. (Note that in the future this may be relaxed so that async/await coroutines can be used with mutual consent of caller/callee; whether or not this is a good idea is up for debate.)
procedure
(<- actor-refr arg ...) → [promise live-refr?]
actor-refr : live-refr? arg : any/c
procedure
actor-refr : live-refr? arg : any/c
procedure
(on vow [ on-fulfilled #:catch on-broken #:finally on-finally #:promise? promise?]) → (or/c live-refr? void?) vow : (or/c live-refr? any/c) on-fulfilled : (or/c procedure? refr? #f) = #f on-broken : (or/c procedure? refr? #f) = #f on-finally : (or/c procedure? refr? #f) = #f promise? : bool? = #f
on-fulfilled, on-broken, and on-finally all, if specified, may be either a procedure (the most common case) which is run when vow becomes resolved, or a reference to an actor that should be messaged when the promise is resolved with the same arguments that would be passed to an equivalent procedure. As their names suggest, on-fulfilled will run if a promise is fulfilled and takes one argument, the fulfilled value. on-broken will run if a promise is broken and takes one argument, the error value. on-finally will run when a promise is resolved, no matter the outcome and is called with no arguments.
If #:promise? is set to #t, then on will itself return a promise. The promise will be resolved as follows:
If vow is fulfilled and on-fulfilled is not provided, the promise returned will share vow’s resolution.
If vow is broken and on-broken is not provided, the promise returned will share vow’s broken result.
If vow is fulfilled and on-fulfilled is provided, the resolution will be whatever is returned from on-fulfilled, unless on-fulfilled raises an error, in which case the promise will be broken with its error-value set to this exception.
If vow is broken and on-broken is provided, the resolution will be whatever is returned from on-broken, unless on-broken raises an error, in which case the promise will be broken with its error-value set to this exception (which may even be the original exception).
1Why "money call"? Because you need $ to make distributed ocap financial instruments!
3.3 References
A reference is a capability to communicate with an actor. References are an indirection and abstractly correspond to an actor handler in an actormap somewhere, often in a vat.
3.3.1 Live vs Sturdy references
The most common kind of reference is a live reference, meaning that they correspond to some actor which we have an established connection to. However some references may be sturdy references, meaning they are serialized in such a way that they probably refer to some actor, but the connection to it is dormant in this reference itself. A sturdy reference must be enlivened with enliven before it can be used. (TODO: or do we just want to reuse <-? Dunno.)
NOTE: Sturdy references aren’t implemented yet, and neither is enliven. Change that!
procedure
(live-refr? obj) → bool?
obj : any/c
procedure
(sturdy-refr? obj) → bool?
obj : any/c
TODO: Define and document enliven, maybe.
3.3.2 Near vs far references
An actor is near if it is in the same vat as the actor being called in an actor context. An actor is far if it is not. The significance here is that only near actors may perform immediate calls with $, whereas any actor may perform asynchronous message sends with <- and <-np.
procedure
(near-refr? obj) → bool?
obj : any/c
3.3.3 Local vs remote references
Well, once machines exist, this will matter, but they don’t yet :P
3.4 Actormaps
An actormap is the key abstraction that maps actor references to their current method handlers. There are actually two kinds of actormaps, whactormaps and transactormaps.
procedure
(make-actormap [#:vat-connector vat-connector]) → whactormap?
vat-connector : (or/c procedure? #f) = #f
Actormaps are also wrapped by vats. More commonly, users will use vats than actormaps directly; however, there are some powerful aspects to doing so, namely for strictly-synchronous programs (such as games) or for snapshotting actormaps for time-traveling purposese.
In general, there are really two key operations for operating on actormaps. The first is actormap-spawn, which is really just used to bootstrap an actormap with some interesting actors. Actors then operate on turns, which are basically a top-level invocation; the core operation for that is actormap-turn. This can be thought of as like a toplevel invocation of a procedure at a REPL: the procedure called may create other objects, instantiate and call other procedures, etc, but (unless some portion of computation goes into an infinite loop) will eventually return to the REPL with some value. Actormap turns are similar; actors may do anything that actors can normally do within the turn, including spawning new actors and calling other actors, but the turn should ideally end in some result (as well as some new messages to possibly dispatch).1Due to the halting problem, this cannot be pre-guaranteed in a turing-complete environment such as what Goblins runs in. Actors can indeed go into an infinite loop; in general the security model of Goblins is to assume that actors in the same vat can thus "hose their vat" (but really this means, an actormap turn might not end on its own, and vats currently don’t try to stop it). Pre-emption can be layered manually though when operating on the actormap directly; if you want to do this, see Suspending, Resuming, and Killing Threads.
3.4.1 Actormap methods
procedure
(actormap-spawn actormap constructor arg ...)
→
[actor-refr live-refr?] [new-actormap transactormap?] actormap : actormap? constructor : procedure? arg : any/c
procedure
(actormap-spawn! actormap constructor arg ...) → [actor-refr live-refr?] actormap : actormap? constructor : procedure? arg : any/c
procedure
(actormap-turn actormap to-refr args ...)
→
[result any/c] [new-actormap transactormap?] [to-near (listof message?)] [to-far (listof message?)] actormap : actormap? to-refr : live-refr? args : any/c
procedure
(actormap-reckless-poke! actormap to-refr arg ...) → [actor-refr live-refr?] actormap : whactormap? to-refr : live-refr? arg : any/c
procedure
(actormap-poke! actormap to-refr args ...) → any/c
actormap : actormap? to-refr : live-refr? args : any/c
procedure
(actormap-peek actormap to-refr args ...) → void?
actormap : actormap? to-refr : live-refr? args : any/c
procedure
(actormap-run actormap proc) → any/c
actormap : actormap? proc : (-> any/c)
Like actormap-peek, this is useful for interrogating an actormap, but can be useful for doing several things at once.
procedure
(actormap-run! actormap proc) → any/c
actormap : actormap? proc : (-> any/c)
3.4.2 whactormap
A whactormap is the default kind of actormap; uses a weak hashtable for mapping.
procedure
(make-whactormap [#:vat-connector vat-connector]) → whactormap?
vat-connector : (or/c procedure? #f) = #f
procedure
(whactormap? obj) → bool?
obj : any/c
3.4.3 transactormap
A transactormap is an actormap that stores a delta of its changes and points at a previous actormap. It must be committed using transactormap-commit! before its changes officially make it into its parent.
procedure
(transactormap? obj) → bool?
obj : any/c
procedure
(make-transactormap parent [ #:vat-connector vat-connector]) → transactormap? parent : actormap? vat-connector : (or/c procedure? #f) = #f
procedure
(transactormap-merge! transactormap) → void/c
transactormap : transactormap?
Note that creating two forking timelines of transactormaps upon a whactormap and merging them may corrupt your whactormap.
procedure
(transactormap-merged? transactormap) → bool?
transactormap : transactormap?
procedure
(transactormap-parent transactormap) → actormap?
transactormap : transactormap?
procedure
(transactormap-delta transactormap) → hasheq?
transactormap : transactormap?
3.4.4 Snapshotting and restoring actormaps
procedure
(snapshot-whactormap whactormap) → hasheq?
whactormap : whactormap?
procedure
(hasheq->whactormap ht) → whactormap?
ht : hasheq?
3.4.5 Extra actormap procedures
(require (submod goblins/core actormap-extra)) |
These are very low level but can be useful for interrogating an actormap.
TODO: document.
3.4.6 Vat connectors
Somewhat awkwardly named since they most visibly show up in actormaps, a vat connector is a procedure (or #f) which is attached to an actormap. It serves two purposes:
To tell whether or not two actor references are near each other. If both share the same vat connector, then they are considered near.2There is one case in which this could be misleading: if both references are spawned in different actormaps that have no vat connector (ie, it is #f), then they likely won’t appear in each others’ vats.
In case actors are not near each other, this specifies how to reach the actor. The procedure is called to communicate with the remote actormap, probably by dropping a message into the queue of its vat event loop.
If you are using make-actormap, this defaults to #f, meaning that all other actors that also have no vat connector will assume they are likewise near. This of course also means that an actor which is in a vat will have no way of communicate with an actor which isn’t.
On the other hand, vats built with make-vat set up their own vat connectors for you.
1Due to the halting problem, this cannot be pre-guaranteed in a turing-complete environment such as what Goblins runs in. Actors can indeed go into an infinite loop; in general the security model of Goblins is to assume that actors in the same vat can thus "hose their vat" (but really this means, an actormap turn might not end on its own, and vats currently don’t try to stop it). Pre-emption can be layered manually though when operating on the actormap directly; if you want to do this, see Suspending, Resuming, and Killing Threads.
2There is one case in which this could be misleading: if both references are spawned in different actormaps that have no vat connector (ie, it is #f), then they likely won’t appear in each others’ vats.
3.5 Vats
A vat1"Vat" might strike you as a strange name; if so, you’re not alone. The term apparently refers to the musing, "How do you know if you’re a brain in a vat?" Previously "vat" was called "hive" in Goblins (and its predecessors, 8sync and XUDD); it was an independent discovery of the same concept in E. Initially, Goblins stuck with "hive" because the primary author of Goblins thought it was a more descriptive term; various ocap people implored the author to not further fragment ocap vocabulary and so the term was switched. Since then, a number of readers of this documentation have complained that "vat" is confusing, and upon hearing this story have asked for the term to be switched back. Whether it’s better to avoid naming fragmentation or possibly increase naming clarity is currently up for debate; your feedback welcome! is an event loop that wraps an actormap. In most cases, users will use vats rather than the more low-level actormaps.
Actors in vats can communicate with actors in other vats on the same machine over a vat connector. For inter-machine communication, see machines. Nonetheless, for the most part users don’t need to worry about this as most inter-vat communication happens using <-.
procedure
(make-vat) → procedure?
Returns a procedure that can be invoked to communicate with the vat (documented below).
The returned procedure uses symbol-based method dispatch.
The procedure returned from make-vat is called the vat dispatcher and is mostly used for bootstrapping or otherwise poking at a vat. Once the actors are bootstrapped in a vat they tend to do their own thing.
The vat dispatcher supports the following symbol-dispatched methods:
'spawn: Behaves like spawn or actormap-spawn!.
'run: Behaves like actormap-run!
'call: Behaves like $ or actormap-poke!
'is-running?: Is this vat still running?
'halt: Stops the next turn from happening in the vat loop. Does not terminate the current turn, but maybe it should.
1"Vat" might strike you as a strange name; if so, you’re not alone. The term apparently refers to the musing, "How do you know if you’re a brain in a vat?" Previously "vat" was called "hive" in Goblins (and its predecessors, 8sync and XUDD); it was an independent discovery of the same concept in E. Initially, Goblins stuck with "hive" because the primary author of Goblins thought it was a more descriptive term; various ocap people implored the author to not further fragment ocap vocabulary and so the term was switched. Since then, a number of readers of this documentation have complained that "vat" is confusing, and upon hearing this story have asked for the term to be switched back. Whether it’s better to avoid naming fragmentation or possibly increase naming clarity is currently up for debate; your feedback welcome!
3.6 Promises
An eventual send with <- returns a promise whose resolution will happen at some future time, either being fulfilled or broken. Fulfilled promises have resolved to a value, whereas broken promises have been resolved with an error.
Every promise has a corresponding resolver which is messaged (often implicitly on completion of a turn)
It turns out that it is possible to make your own promises using spawn-promise-values or spawn-promise-cons, both of which return a promise / resolver pair.
procedure
(spawn-promise-values) →
[promise live-refr?] [resolver live-refr?]
procedure
→ (cons/c [promise live-refr?] [resolver live-refr?])
Promises in Goblins work closer to E than some other languages like Javascript; notable exceptions are that on is used rather than ".then() sausages". Likewise, having a separate resolver object also comes from E.
One major, but perhaps not very important, difference that may not be obvious is that once a promise pointed to by a reference is resolved to something, it for all intents and purposes appears to act just like that thing. If the resolution is to a near reference, it can even be immediately called with "$". However, you still need to know when you can finally dollar-call such a thing, thus you still need to use on anyway.
3.7 Machines
A machine is another layer of abstraction you don’t need to worry about... yet! ;)