Goblins documentation: a draft rewrite
Table of Contents
- 1. What is Goblins?
- 2. A tutorial
- 3. Core API
- 4. Actormap API
- 5. Vat API
- 6. Distributed Goblins
This is a draft of a rewrite of the documentation for Goblins.
1 What is Goblins?
Goblins is a quasi-functional distributed object system. Its design allows for object-capability security, allowing for safe distributed programming environments. Its design is inspired by the E programming language and by the observation that lambda is already the ultimate security mechanism (ie, normal argument-passing in programs, if taken seriously/purely, is already all the security system we need).
1.1 What works so far?
- Quasi-functional object system: Users hold on to references of objects/actors, but objects/actors are just procedures. Rather than directly mutating, objects/actors can specify that they should "become" a new version of themselves when they handle the next invocation.
- Transactional updates: Changes happen within a transaction. If an unhandled exception occurs, we can "roll back" history as if the message never happened, avoiding confused state changes throughout the system.
- Time travel: We can snapshot old revisions of the system and interact with them.
- Asynchronous programming with sophisticated promise chaining: Asynchronous message passing with promises is supported. Promises work such that there's no need to wait for promises to resolve before interacting… you can communicate with the future that doesn't even yet exist! Sent a message to that remote car factory asking for a new car? Why wait for the car to be delivered… you can send it a drive message at the same time, and both messages can be delivered across the network in a single hop.
- Communicating event loops: Objects across multiple event loops (aka "vats") can communicate, whether in the same OS process or (soon) across the network.
- Synchronous and asynchronous behavior, integrated but distinguished: Both synchronous and asynchronous programming is supported, but only objects in the same "vat" (event loop) can perform synchronous immediate calls. All objects can perform asynchronous calls against each other.
- A library, not a language: Goblins is itself a library that can be utilized with nearly any Racket program, including many Racket #langs. (However, some languages may be provided or encouraged for additional security / convenience later).
- Object capability security: Goblins itself is built for object capability (ocap) security, which is to say that you can only operate on the references you have access to. Goblins embraces and builds upon this foundation. (However, full ocap security will require a safer module system than the one Racket provides; coming eventually.)
1.2 What's on its way?
- Fully distributed, networked, secure p2p communication: In the future, it will be possible to communicate with other objects over a network connection. Distributed object interactions is safe because of ocap security guarantees.
- A separate module system: While not part of Goblins itself, a future project named "Spritely Dungeon" will help close the loop on ocap security for Racket.
2 A tutorial
2.1 Vats, actors, spawning, and immediate calls
Let's open a Racket REPL and import Goblins:
(require goblins)
First we're going to need something to store our objects in. We'll boot up an event loop, called a "vat" (TODO: explain why it's called that), which can manage our objects.
(define a-vat (make-vat))
Our vat is currently lonely… nobody lives in it! Let's make a friend. First we'll need a friend constructor:
;; The ^ is conventionally called a "hard hat" in Goblins; it means ;; this is an object constructor. ;; Every constructor takes a bcom argument, the rest of them are ;; passed in from the spawn invocation. (define (^friend bcom my-name) ;; This is the initial handler procedure. (lambda (your-name) (format "Hello ~a, my name is ~a!" your-name my-name)))
The outer procedure is the constructor; all it really does is return another procedure which is the handler.
Let's make a friend and call her Alice.
(define alice (a-vat 'spawn ^friend "Alice"))
Here the arguments to the spawn method are ^friend, which is the
constructor procedure we are using, and the argument "Alice", which
becomes bound to my-name
.
(The bcom
argument is implicitly provided by Goblins; we'll ignore
it for right now.)
If we look at Alice in the REPL, we'll see that what we're really holding onto is a "live reference" to Alice.
> alice
#<live-refr ^friend>
Now we'd like to talk to Alice. For now let's use the "call" method on our vat:
> (a-vat 'call alice "Chris") "Hello Chris, my name is Alice!"
If we look at our ^friend
procedure again, this should make a lot of
sense; the inner lambda is being evaluated with "Chris" being passed in
for your-name
.
The returned value is just the result.
Normally in a Goblins program, we can just spawn and talk to a Goblins
object directly using the spawn
and $
operators directly.
However we're kind of "bootstrapping the world" here, so we need the
vat's help to do that.
However, we can see what it would look like to use them if we were in
a "Goblins context" by using the vat's 'run
method which allows us
to pass in an arbitrary thunk (aka "procedure with no arguments"):
> (a-vat 'run (lambda () (define alyssa (spawn ^friend "Alyssa")) ($ alyssa "Ben"))) "Hello Ben, my name is Alyssa!"
Anyway, maybe we'd like to be greeted the same way we have been by our
friend sometimes, but other times we'd like to just find out what our
friend's name is.
It would be nice to have different "methods" we could call, and in
fact, Goblins comes with a convenient methods
macro:
(require goblins/actor-lib/methods) (define (^methods-friend bcom my-name) (methods ;; Greet the user using their name [(greet your-name) (format "Hello ~a, my name is ~a!" your-name my-name)] ;; return what our name is [(name) my-name]))
Now let's spawn an alice2 that uses ^methods-friend
:
(define alice2 (a-vat 'spawn ^methods-friend "Alice"))
Now we can call each method separately:
> (a-vat 'call alice2 'name) "Alice" > (a-vat 'call alice2 'greet "Chris") "Hello Chris, my name is Alice!"
(As a side note, if you're thinking that it would look nicer if this looked like:)
> (a-vat.call alice2.name) "Alice" > (a-vat.call alice2.greet "Chris") "Hello Chris, my name is Alice!"
(… you're right and a #lang
will provided in the future for such
aesthetic improvement which expands to the former syntax.)
What kind of magic is this methods thing? Well actually it's barely any magic at all. Methods just returns a procedure that dispatches on the first argument, a symbol, to one of several procedures. We can even use it outside of an object/actor constructor:
> (define what-am-i (methods [(i-am) (list 'i 'am 'just 'a 'procedure)] [(that what) (list 'that 'calls 'other what)])) > (what-am-i 'i-am) '(i am just a procedure) > (what-am-i 'that 'procedures) '(that calls other procedures)
We could have just as well written methods-friend like so:
(define (^match-friend bcom my-name) (match-lambda* ;; Greet the user using their name [(list 'greet your-name) (format "Hello ~a, my name is ~a!" your-name my-name)] ;; return what our name is [(list 'name) my-name]))
But it's a lot nicer to use methods
.
But the key thing to realize is that methods
just itself returns
another procedure.
Maybe we'd like to keep track of how many times our friend has been called. It might be helpful to have some kind of helper object which can do that. What if we made a counter?
(define (^counter bcom [count 0]) (methods ;; return the current count [(count) count] ;; Add one to the current counter [(add1) (bcom (^counter bcom (add1 count)))]))
Now let's spawn and poke at an instance of that counter a bit:
> (define a-counter (a-vat 'spawn ^counter)) > (a-vat 'call a-counter 'count) 0 > (a-vat 'call a-counter 'add1) > (a-vat 'call a-counter 'count) 1 > (a-vat 'call a-counter 'add1) > (a-vat 'call a-counter 'add1) > (a-vat 'call a-counter 'count) 3
Now note that our counter actually appears to change… how does this happen? Let's look at the body of that add1 method in detail:
(bcom (next (add1 count)))
bcom
(pronounced "be-come" or "be-comm") is the capability to
"become" a new version of ourselves; more explicitly, to give a
procedure which will be called the next time this object is invoked.
next
returns some methods wrapped up in a closure which knows
what the count is; we're incrementing that by one from whatever
we currently have.
(Depending on how experienced you are with functional programming is
how confusing this is likely to be.)
There are multiple equivalent ways we could build the "next" procedure
we are becoming for ourselves, and one is to build an actual next
builder and instantiate it once.
This technique is exactly equivalent to the above, and we will use this
kind of structure later, so it's worth seeing and realizing it's more
or less the same (except that we didn't expose the choice of an initial
count value):
(define (^counter bcom) (define (next count) (methods ;; return the current count [(count) count] ;; Add one to the current counter [(add1) ;; Become the next version of ourselves, ;; with count incremented (bcom (next (add1 count)))])) ;; We'll start at 0. (next 0))
Now that we have this counter, we can rewrite our friend to spawn it and use it:
(define (^counter-friend bcom my-name) (define greet-counter (spawn ^counter)) (methods ;; Greet the user using their name [(greet your-name) ;; Increment count by one, since we were just called. ;; The counter starts at 0 so this will be correct. ($ greet-counter 'add1) (define greet-count ($ greet-counter 'count)) (format "Hello ~a, my name is ~a and I've greeted ~a times!" your-name my-name greet-count)] ;; return what our name is [(name) my-name] ;; check how many times we've greeted, without ;; incrementing it [(greet-count) ($ greet-counter 'count)]))
You'll observe that there was no need to go through the vat here,
our object was able to use spawn
and $
(which is pronounced "call",
"immediate call", or "money call") directly.
That's because our actor is operating within a goblins context, so
there's no reason to do so by bootstrapping through the vat (indeed,
trying to do so would cause an exception to be raised).
Now let's give it a try:
> (define alice3 (a-vat 'spawn ^counter-friend "Alice")) > (a-vat 'call alice3 'greet "Chris") "Hello Chris, my name is Alice and I've greeted 1 times!" > (a-vat 'call alice3 'greet "Chris") "Hello Chris, my name is Alice and I've greeted 2 times!" > (a-vat 'call alice3 'greet-count) 2 > (a-vat 'call alice3 'greet "Chris") "Hello Chris, my name is Alice and I've greeted 3 times!" > (a-vat 'call alice3 'greet-count) 3
Perhaps we'd like to have our friend remember the last person she was called by. It would be nice if there were something along the lines of a mutable variable we could change. In fact there is, and it's called a cell. When called with no arguments, a cell returns its current value. When called with one argument, a cell replaces its current value with the one we have provided:
> (require goblins/actor-lib/cell) > (define treasure-chest (a-vat 'spawn ^cell 'gold)) > (a-vat 'call treasure-chest) 'gold > (a-vat 'call treasure-chest 'flaming-sword) > (a-vat 'call treasure-chest) 'flaming-sword
A fun exercise is to try to write your own cell. Here is one way:
;; Constructor for a cell. Takes an optional initial value, defaults ;; to false. (define (^our-cell bcom [val #f]) (case-lambda ;; Called with no arguments; return the current value [() val] ;; Called with one argument, we become a version of ourselves ;; with this new value [(new-val) (bcom (^our-cell bcom new-val))]))
Of course, you could also have a cell that instead has 'get
and
'set
methods.
This is left as an exercise for the reader.
Now that we have cells, we can use them:
(define (^memory-friend bcom my-name) (define greet-counter (spawn ^counter)) (define recent-friend (spawn ^cell #f)) (methods ;; Greet the user using their name [(greet your-name) ;; Increment count by one, since we were just called. ;; The counter starts at 0 so this will be correct. ($ greet-counter 'add1) (define greet-count ($ greet-counter 'count)) ;; Who our friend was last time (define last-friend-name ($ recent-friend)) ;; But now let's set the recent friend to be this name ($ recent-friend your-name) (if last-friend-name (format "Hello ~a, my name is ~a and I've greeted ~a times (last by ~a)!" your-name my-name greet-count last-friend-name) (format "Hello ~a, my name is ~a and I've greeted ~a times!" your-name my-name greet-count))] ;; return what our name is [(name) my-name] ;; check how many times we've greeted, without ;; incrementing it [(greet-count) ($ greet-counter 'count)]))
Let's try interacting with this friend:
> (define alice4 (a-vat 'spawn ^memory-friend "Alice")) > (a-vat 'call alice4 'greet "Chris") "Hello Chris, my name is Alice and I've greeted 1 times!" > (a-vat 'call alice4 'greet "Carl") "Hello Carl, my name is Alice and I've greeted 2 times (last by Chris)!" > (a-vat 'call alice4 'greet "Carol") "Hello Carol, my name is Alice and I've greeted 3 times (last by Carl)!"
Whew… if we look at that code for the greet
code, it sure looks
fairly imperative, though.
We pulled out the value from recent-friend
before we changed
it.
If we had accidentlaly put the definition for last-friend-name
after setting recent-friend
to your-name
, we might have resulted in
a classic imperative error and last-friend-name
would be set to the
new one instead of the old one!
Well, it turns out that bcom
can take a second argument which
provides a value it would like to return in addition to specifying the
new version of itself it would like to become.
This means that we could rewrite ^memory-friend
like so with no
behavioral differences:
(define (^memory-friend2 bcom my-name) (define (next greet-count last-friend-name) (methods ;; Greet the user using their name [(greet your-name) (define greeting (if last-friend-name (format "Hello ~a, my name is ~a and I've greeted ~a times (last by ~a)!" your-name my-name greet-count last-friend-name) (format "Hello ~a, my name is ~a and I've greeted ~a times!" your-name my-name greet-count))) (bcom (next (add1 greet-count) your-name) greeting)] ;; return what our name is [(name) my-name] ;; check how many times we've greeted, without ;; incrementing it [(greet-count) greet-count])) (next 1 #f))
> (define alice5 (a-vat 'spawn ^memory-friend2 "Alice")) > (a-vat 'call alice5 'greet "Chris") "Hello Chris, my name is Alice and I've greeted 1 times!" > (a-vat 'call alice5 'greet "Carl") "Hello Carl, my name is Alice and I've greeted 2 times (last by Chris)!" > (a-vat 'call alice5 'greet "Carol") "Hello Carol, my name is Alice and I've greeted 3 times (last by Carl)!"
This certainly looks more functional, and we have some freedom of how we'd like to implement it. It also leads to the observation that the behavior of objects in respect to updating themselves appears to be very functional (returning a new version of ourselves and maybe a value), whereas calling other objects appears to be very imperative.
So what is the point of cells and counters?
After all, if we're using #lang racket
we have access to the set!
procedure, and could have easily rewritten the ^counter
and ^cell
versions like so:
(define (^imperative-friend bcom my-name) (define greet-count 0) (define recent-friend #f) (methods ;; Greet the user using their name [(greet your-name) ;; Increment count by one, since we were just called. ;; The counter starts at 0 so this will be correct. (set! greet-count (add1 greet-count)) ;; Who our friend was last time (define last-friend-name recent-friend) ;; But now let's set the recent friend to be this name (set! recent-friend your-name) (if last-friend-name (format "Hello ~a, my name is ~a and I've greeted ~a times (last by ~a)!" your-name my-name greet-count last-friend-name) (format "Hello ~a, my name is ~a and I've greeted ~a times!" your-name my-name greet-count))] ;; return what our name is [(name) my-name] ;; check how many times we've greeted, without ;; incrementing it [(greet-count) greet-count] [(recent-friend) recent-friend]))
Usage is exactly the ssame:
> (define alice6 (a-vat 'spawn ^imperative-friend "Alice")) > (a-vat 'call alice6 'greet "Chris") "Hello Chris, my name is Alice and I've greeted 1 times!" > (a-vat 'call alice6 'greet "Carl") "Hello Carl, my name is Alice and I've greeted 2 times (last by Chris)!" > (a-vat 'call alice6 'greet "Carol") "Hello Carol, my name is Alice and I've greeted 3 times (last by Carl)!"
This code looks mostly the same too, and indeed maybe even a little
simpler with set!
(no mucking around with that $
malarky).
Let's introduce a couple of bugs into both the cell-using and
the set!
using imperative versions of these friends:
(define (^buggy-memory-friend bcom my-name) (define greet-counter (spawn ^counter)) (define recent-friend (spawn ^cell #f)) (methods ;; Greet the user using their name [(greet your-name) ;; Increment count by one, since we were just called. ;; The counter starts at 0 so this will be correct. ($ greet-counter 'add1) (define greet-count ($ greet-counter 'count)) ;; Who our friend was last time (define last-friend-name ($ recent-friend)) ;; But now let's set the recent friend to be this name ($ recent-friend your-name) (error "AHH! Throwing an error after I changed things!") (if last-friend-name (format "Hello ~a, my name is ~a and I've greeted ~a times (last by ~a)!" your-name my-name greet-count last-friend-name) (format "Hello ~a, my name is ~a and I've greeted ~a times!" your-name my-name greet-count))] ;; return what our name is [(name) my-name] ;; check how many times we've greeted, without ;; incrementing it [(greet-count) ($ greet-counter 'count)])) (define (^buggy-imperative-friend bcom my-name) (define greet-count 0) (define recent-friend #f) (methods ;; Greet the user using their name [(greet your-name) ;; Increment count by one, since we were just called. ;; The counter starts at 0 so this will be correct. (set! greet-count (add1 greet-count)) ;; Who our friend was last time (define last-friend-name recent-friend) ;; But now let's set the recent friend to be this name (set! recent-friend your-name) (error "AHH! Throwing an error after I changed things!") (if last-friend-name (format "Hello ~a, my name is ~a and I've greeted ~a times (last by ~a)!" your-name my-name greet-count last-friend-name) (format "Hello ~a, my name is ~a and I've greeted ~a times!" your-name my-name greet-count))] ;; return what our name is [(name) my-name] ;; check how many times we've greeted, without ;; incrementing it [(greet-count) greet-count]))
(Observe the error
put after both of them changed greet-count
and recent-friend
.)
Okay, first let's check the initial greet-count
:
> (a-vat 'call buggy-gobliny-alice 'greet-count) 0 > (a-vat 'call buggy-imperative-alice 'greet-count) 0
So far, so good. Now let's greet both of them:
> (a-vat 'call buggy-gobliny-alice 'greet "Chris") ; AHH! Throwing an error after I changed things! ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:498:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:903:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:433:0 call-with-fresh-syscaller ; /gnu/store/xpivjjmjgcc3l3415dcvb4pm5xrbrm3i-racket-7.3/share/racket/collects/racket/match/compiler.rkt:507:40 f71 > (a-vat 'call buggy-imperative-alice 'greet "Chris") ; AHH! Throwing an error after I changed things! ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:498:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:903:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:433:0 call-with-fresh-syscaller ; /gnu/store/xpivjjmjgcc3l3415dcvb4pm5xrbrm3i-racket-7.3/share/racket/collects/racket/match/compiler.rkt:507:40 f71
Okay, so both of them threw the error.
But what do you think the result of greet-count
will be now?
> (a-vat 'call buggy-gobliny-alice 'greet-count) 0 > (a-vat 'call buggy-imperative-alice 'greet-count) 1
Now this is definitely different!
In the goblin'y example, by using goblin objects/actors, unhandled
errors means that breakage is as if nothing ever occurred.
We can log the error, but we won't mess up the rest of the system.
With the imperative code which uses set!
, the state of our system
could become unintentionally corrupted and inconsistent.
This is what we mean by Goblins being transactional: something that goes wrong need not be "committed" to the current state. This is important for systems like financial infrastructure. It turns out it also opens us up, in general, to becoming time wizards. But more on that later.
2.2 Message passing, promises, and multiple vats
2.2.1 The basics
Remember simpler times, when friends mostly just greeted us hello?
(define (^friend bcom my-name) (lambda (your-name) (format "Hello ~a, my name is ~a!" your-name my-name))) (define alice (a-vat 'spawn ^friend "Alice"))
We could of course make another friend that talks to Alice.
(define (^calls-friend bcom our-name) (lambda (friend) (define what-my-friend-said ($ friend our-name)) (displayln (format "<~a>: I called my friend, and they said:" our-name)) (displayln (format " \"~a\"" what-my-friend-said)))) (define archie (a-vat 'spawn ^calls-friend "Archie"))
Now Archie can talk to Alice:
> (a-vat 'call archie alice) <Archie>: I called my friend, and they said: "Hello Archie, my name is Alice!"
Both Alice and Archie live in a-vat
.
But a-vat
isn't the only vat in town. One other such vat
is b-vat
, where Bob lives:
(define b-vat (make-vat)) (define bob (b-vat 'spawn ^calls-friend "Bob"))
Obviously, since Bob is in b-vat
, we bootstrap a message call to Bob
from b-vat
.
But what do you think happens when Bob tries to call Alice?
> (b-vat 'call bob alice) ; not-callable: Not in the same vat: #<live-refr ^friend> ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:542:5 ; /home/cwebber/sandbox/goblins-tut.rkt:240:2 ; /home/cwebber/devel/goblins/goblins/core.rkt:498:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:903:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:433:0 call-with-fresh-syscaller ; /gnu/store/xpivjjmjgcc3l3415dcvb4pm5xrbrm3i-racket-7.3/share/racket/collects/racket/match/compiler.rkt:507:40 f71
Oh no!
It looks like Bob can't call Alice since they live in different
places!
From Archie's perspective, Alice was "near", aka "in the same vat".
However from Bob's perspective Alice was "far", aka "in some other
vat that isn't the one I'm in".
This is a problem because using the $
operator performs a
synchronous call, but it's only safe to do synchronous calls for
objects that are near each other (in the same vat).
Fortunately there's something we can do: we can send a message from Bob to Alice. But we've never seen message sending in Goblins before, so what is that?
To prototype this, let's use the 'run
method on b-vat
.
Remember, we saw the 'run
method used before, where it looked like:
> (a-vat 'run (lambda () (define alyssa (spawn ^friend "Alyssa")) ($ alyssa "Ben"))) "Hello Ben, my name is Alyssa!"
So 'run
is just a way to run some arbitrary code in an actor context.
That sounds good enough for playing around with sending messages.
We can send messages with <-
so let's try that:
> (b-vat 'run (lambda () (<- alice "Brenda"))) #<live-refr promised>
Ah ok… so what <-
returns is something called a "Promise" which
might eventually be resolved to something interesting.
We want some way to be able to pull out that interesting thing.
That's what on
is for: it resolves promises and pulls out their
resolved value:
> (b-vat 'run (lambda () (on (<- alice "Brenda") (lambda (alice-says) (displayln (format "Got from Alice: ~a" alice-says)))))) Got from Alice: Hello Brenda, my name is Alice!
<-
works just fine with far references, but it also works just fine
with near references too!
So we can run the same code in a-vat (where Alice is "near") and it
works there too:
> (a-vat 'run (lambda () (on (<- alice "Arthur") (lambda (alice-says) (displayln (format "Got from Alice: ~a" alice-says)))))) Got from Alice: Hello Arthur, my name is Alice!
So using on
and <-
seems to fit our needs.
But what would have happened if Alice had thrown an error?
Indeed, if we remember earlier we made buggy-gobliny-alice
so we can test for that.
It turns out that on can take a #:catch
argument:
> (b-vat 'run (lambda () (on (<- buggy-gobliny-alice 'greet "Brenda") (lambda (alice-says) (displayln (format "Got from Alice: ~a" alice-says))) #:catch (lambda (err) (displayln "Tried to talk to Alice, got an error :("))))) ;; === While attempting to send message: === ; AHH! Throwing an error after I changed things! ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:498:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:966:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:433:0 call-with-fresh-syscaller ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp Tried to talk to Alice, got an error :(
Now this is a little bit confusing to read because we saw two separate
messages here… it's important to realize that due to the way our vat
is configured, the exception backtrace being printed out is coming
from a-vat
, not from our code being evaluated in b-vat
.
We could configure the a-vat
loop to do something different when it
hits errors, but currently it prints exceptions so we can debug them.
Anyway, so that's helpful information, but actually the place we caught
the error in our code above was in the lambda
right after #:catch
.
As we can see, it did catch the error and we used that as an opportunity
to print out a complaint.
So <-
makes a promise for us.
We don't always need a promise; sometimes we're just calling something
for its effects.
For instance we might have a parrot that we like to encourage to say
silly things, maybe on the screen or even out loud, but we don't care
much about the result.
In that case we can use <-np
which sends a message but with "no
promise":
> (define parrot (a-vat 'spawn (lambda (bcom) (lambda (phrase) ;; Since we're using displayln, we're printing this phrase ;; rather than returning it as a string (displayln (format "<parrot>: ~a... *SQWAK!*" phrase)))))) > (b-vat 'run (lambda () (<-np parrot "Polly wants a chiptune"))) <parrot>: Polly wants a chiptune... *SQWAK!*
When we don't need a promise, <-np
is an optimization that saves us
from some promise overhead.
But in most of our code, <-
performs the more common case
of returning a promise.
Anyway, we should have enough information to make a better constructor
for friends who are far away.
Recall our definition of ^calls-friend
:
(define (^calls-friend bcom our-name) (lambda (friend) (define what-my-friend-said ($ friend our-name)) (displayln (format "<~a>: I called my friend, and they said:" our-name)) (displayln (format " \"~a\"" what-my-friend-said))))
.. we'll make a few changes and name our constructor ^messages-friend
:
(define (^messages-friend bcom our-name) (lambda (friend) (on (<- friend our-name) (lambda (what-my-friend-said) (displayln (format "<~a>: I messaged my friend, and they said:" our-name)) (displayln (format " \"~a\"" what-my-friend-said))) #:catch (lambda (err) (displayln "I messaged my friend but they broke their response promise...")))))
(We even made it a bit more robust than our previous implementation by handling errors!)
Now we can make a version of Bob that can do a better job of holding a conversation with his far-away friend Alice:
> (b-vat 'call bob2 alice) <Bob>: I messaged my friend, and they said: "Hello Bob, my name is Alice!"
Much better!
2.2.2 Making and resolving our own promises
So we know that <-
can make promises, but it turns out we can
make promises ourselves:
> (a-vat 'run spawn-promise-cons) '(#<live-refr promised> . #<live-refr ^resolver>)
As we can see, promises come in pairs: the promise object, which we
can listen to with on
, and the resolver object, which lets us
fulfill or break a promise.
We can also use spawn-promise-pair
to spawn a promise
(TODO: add footnote about why we didn't earlier, multiple value return
not being allowed from actors currently), which returns multiple
values (which we can bind with define-values
).
We can then try resolving a promise with on
… but of course
we'll need to fulfill or break it to see anything.
> (a-vat 'run (lambda () (define-values (foo-vow foo-resolver) (spawn-promise-pair)) (define-values (bar-vow bar-resolver) (spawn-promise-pair)) (define (declare-resolved result) (printf "Resolved: ~a\n" result)) (define (declare-broken err) (printf "Broken: ~a\n" err)) (on foo-vow declare-resolved #:catch declare-broken) (on bar-vow declare-resolved #:catch declare-broken) ($ foo-resolver 'fulfill 'yeah-foo) ($ bar-resolver 'break 'oh-no-bar))) Resolved: yeah-foo Broken: oh-no-bar
By the way, you may notice that there's a naming convention in Goblins
(borrowed from E) to append a -vow
suffix if something is a promise
(or should be treated as one).
That's a good practice for you to adopt, too.
2.2.3 Finally we have #:finally
Maybe we'd like to run something once a promise resolves, regardless
of whether or not it succeds or fails.
In such a case we can use the #:finally
keyword:
> (a-vat 'run (lambda () (define resolves-ok (spawn (lambda (bcom) (lambda () "This is fine!")))) (define errors-out (spawn (lambda (bcom) (lambda () (error "I am error!"))))) (define (handle-it from-name vow) (on vow (lambda (val) (displayln (format "Got from ~a: ~a" from-name val))) #:catch (lambda (err) (displayln (format "Error from ~a: ~a" from-name err))) #:finally (lambda () (displayln (format "Done handling ~a." from-name))))) (handle-it 'resolves-ok (<- resolves-ok)) (handle-it 'errors-out (<- errors-out)))) ;; === While attempting to send message: === ; I am error! ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:500:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:967:0 actormap-turn-message86 ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp Got from resolves-ok: This is fine! Done handling resolves-ok. Error from errors-out: #(struct:exn:fail I am error! #<continuation-mark-set>) Done handling errors-out.
2.2.4 the on-fulfilled handler of "on" is optional
Maybe all you care about is the #:catch
or #:finally
clause.
The on-fulfilled
argument to on
is actually optional:
> (define ((^throws-error bcom)) (error "oh no")) > (define throws-error (a-vat 'spawn ^throws-error)) > (a-vat 'run (lambda () (on (<- throws-error) #:catch (lambda (err) (displayln (format "The error is: ~a" err)))))) ;; === While attempting to send message: === ; oh no ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:500:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:985:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:435:0 call-with-fresh-syscaller ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp The error is: #(struct:exn:fail oh no #<continuation-mark-set>)
(Side note, this is the first time we've seen the procedure definition
style used in ^throws-error
… it's a way in Racket of making a
procedure that defines a procedure. Handy in Goblins! If you're new
to that, these two definitions are equivalent:)
(define (^friend bcom my-name) (lambda (your-name) (format "Hello ~a, my name is ~a!" your-name my-name))) (define ((^friend bcom my-name) your-name) (format "Hello ~a, my name is ~a!" your-name my-name))
2.2.5 "on" with non-promise values
on
works just fine if you pass in a non-promise value.
It'll just treat that value as if it were a promise that had
resolved immediately.
For example:
> (a-vat 'run (lambda () (on 5 (lambda (v) (displayln (format "Got: ~a" v)))))) Got: 5
2.2.6 "on" can return promises too
It turns out that "on" can also return a promise!
However, this isn't a very common use case, so you have to ask for it
via the #:promise?
keyword.
> (define ((^bakery bcom name) carb) (format "~a's signature ~a baking" name carb)) > (define petite-oven-bakery (a-vat 'spawn ^bakery "The Petite Oven")) > (a-vat 'run (lambda () (define smell-vow (on (<- petite-oven-bakery "croissants") (lambda (what-you-smell) (format "You smell ~a. Heavenly!!" what-you-smell)) #:promise? #t)) (on smell-vow displayln))) You smell The Petite Oven's signature croissants baking. Heavenly!!
The choice of whether or not to include #:catch
in an on
with
#:promise?
affects whether or how an error will propagate (or be
cleaned up).
Without catching an error:
> (define ((^throws-error bcom)) (error "oh no")) > (define throws-error (spawn ^throws-error)) > (a-vat 'run (lambda () (on (on (<- throws-error) (lambda (val) (displayln "I won't run...")) #:promise? #t) #:catch (lambda (e) (displayln (format "Caught: ~a" e)))))) ;; === While attempting to send message: === ; oh no ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:500:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:985:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:435:0 call-with-fresh-syscaller ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp Caught: #(struct:exn:fail oh no #<continuation-mark-set>)
However if we catch the error, we can return a value that will succeed if we like:
> (a-vat 'run (lambda () (on (on (<- throws-error) #:catch (lambda (e) "An error? Psh... this is fine.") #:promise? #t) (lambda (val) (displayln (format "Got: ~a" val)))))) ;; === While attempting to send message: === ; oh no ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:500:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:985:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:435:0 call-with-fresh-syscaller ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp Got: An error? Psh... this is fine.
But if our #:catch
handler raises an error, that error will simply
propagate (instead of the original one):
> (a-vat 'run (lambda () (on (on (<- throws-error) #:catch (lambda (e) (error "AAAAH! This is NOT fine!")) #:promise? #t) #:catch (lambda (e) (displayln (format "Caught: ~a" e)))))) ;; === While attempting to send message: === ; oh no ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:500:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:985:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:435:0 call-with-fresh-syscaller ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp ;; === While attempting to send message: === ; AAAAH! This is NOT fine! ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:897:11 ; /home/cwebber/devel/goblins/goblins/core.rkt:500:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:985:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:435:0 call-with-fresh-syscaller ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp Caught: #(struct:exn:fail AAAAH! This is NOT fine! #<continuation-mark-set>)
2.2.7 Promise pipelining
Machines grow faster and memories grow larger. But the speed of light is constant and New York is not getting any closer to Tokyo.
– from Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control, by Mark S. Miller
Let's say we have a car factory that makes cars:
(define (^car-factory bcom company-name) (define ((^car bcom model color)) (format "*Vroom vroom!* You drive your ~a ~a ~a!" color company-name model)) (define (make-car model color) (spawn ^car model color)) make-car) (define fork-motors (a-vat 'spawn ^car-factory "Fork"))
Now observe… in this scenario, Fork Motors exists on a-vat
.
It will also generate a car that technically lives on a-vat
.
Let's say we live on b-vat… we'd still like to drive our car.
Doing so seems extremely ugly:
> (b-vat 'run (lambda () (on (<- fork-motors "Explorist" "blue") (lambda (our-car) (on (<- our-car) displayln))))) *Vroom vroom!* You drive your blue Fork Explorist!
This is hard to follow. Maybe if we actually name some of those promises it'll be a bit easier to read:
> (b-vat 'run (lambda () (define car-vow (<- fork-motors "Explorist" "blue")) (on car-vow (lambda (our-car) (define drive-noise-vow (<- our-car)) (on drive-noise-vow displayln))))) *Vroom vroom!* You drive your blue Fork Explorist!
Hm, not so much better.
Naming things helped us remember what each promise was for, but it
seems to be the nesting of on
handlers that's confusing.
Fortunately, Goblins supports something called "promise pipelining". We can send an instruction to drive our car… before it even rolls off the factory lot! That's right… you can send messages to promises, before they have even resolved!
> (b-vat 'run (lambda () (define car-vow (<- fork-motors "Explorist" "blue")) (define drive-noise-vow (<- car-vow)) (on drive-noise-vow displayln))) *Vroom vroom!* You drive your blue Fork Explorist!
Wow… that's much easier to read!
But readability is not the only goal of promise pipelining. Like many things in Goblins, promise pipelining comes from the E programming language. We're still working on distributed networked Goblins code, but the foundations are there to do the same thing that E does: send messages with less round trips!
Think about it this way: if a-vat
actually lived on a different
server from b-vat
, and both servers lived halfway across the globe
with the first way we implemented things:
b-vat
would have to first send a message to the factory ona-vat
first asking to make a car- then
a-vat
would have to respond, resolving the promise with the location of the new car - then
b-vat
would have to send another message asking to drive the car - then
a-vat
would have to respond, resolving yet another promise with the noise the car makes - and finally
b-vat
can now display the car noise to the user.
With promise pipelining, this merely becomes:
b-vat
would send a message to the factory ona-vat
first asking to make a car and at the same time can say, "and once this car is made, I'd like to send another message to it so I can drive it."- then
a-vat
can make the car, drive the car, and then respond with the noise the car makes - and now
b-vat
can display the car noise to the user.
Instead of going B => A => B => A => B
, we have reduced our work to
B => A => B
… a significant savings in round-trips.
This can really add up in distributed applications, where (as Miller's
quote at the top of this subsection indicates) we may be limited in
how much we can prevent network delays by physics itself.
2.2.8 Broken promise contagion
Of course, now that we know that we can pipe promises together, what happens if an error happens in the middle of the pipeline?
(define (^lessgood-car-factory bcom company-name) (define ((^car bcom model color)) (format "*Vroom vroom!* You drive your ~a ~a ~a!" color company-name model)) (define (make-car model color) (error "Your car exploded on the factory floor! Ooops!") (spawn ^car model color)) make-car) (define forked-motors (a-vat 'spawn ^lessgood-car-factory "Forked"))
The answer is that the error is simply propagated to all pending promises:
> (b-vat 'run (lambda () (define car-vow (<- forked-motors "Exploder" "red")) (define drive-noise-vow (<- car-vow)) (on drive-noise-vow displayln #:catch (lambda (err) (displayln (format "Caught: ~a" err)))))) ;; === While attempting to send message: === ; Your car exploded on the factory floor! Ooops! ; Context: ; /home/cwebber/devel/goblins/goblins/core.rkt:500:5 ; /home/cwebber/devel/goblins/goblins/core.rkt:989:3 ; /home/cwebber/devel/goblins/goblins/core.rkt:435:0 call-with-fresh-syscaller ; /home/cwebber/devel/goblins/goblins/vat.rkt:135:11 lp Caught: #(struct:exn:fail Your car exploded on the factory floor! Ooops! #<continuation-mark-set>)
2.3 Going low-level: actormaps
2.3.1 Spawning, peeking, poking, turning
So far we have dealt with vats, but there is a lower level of abstraction in Goblins which is called an "actormap".
The key differences are:
- vats: An event loop implementation that runs continuously in its own thread. Comes pre-built with tooling to handle message passing between vats. Actually wraps an actormap.
- actormap: The transactional datastructure that vats use. But, useful on its own to either build your own event loop or as a synchronous one-turn-at-a-time mapping of actor references to their current implementations.
The best way to learn is by doing, so let's make an actormap now:
(define am (make-actormap))
Let's add an actor to it. Since cells are simple, let's add one of those.
(define lunchbox (actormap-spawn! am ^cell))
What's in the lunchbox?
We could take a look with actormap-peek
:
> (actormap-peek am lunchbox) #f
An empty lunchbox… we'd like to eat something later, so how about we
pack ourselves a nice sandwich by using actormap-poke
:
> (actormap-poke! am lunchbox 'peanut-butter-and-jelly) > (actormap-peek am lunchbox) 'peanut-butter-and-jelly
What if we tried to look inside the lunchbox with actormap-poke! instead of actormap-peek?
> (actormap-poke! am lunchbox 'bbq-tofu) > (actormap-poke! am lunchbox) 'bbq-tofu
Well that worked just fine.
What happens if we do both operations with actormap-peek
?
> (actormap-peek am lunchbox 'chickpea-salad) > (actormap-peek am lunchbox) 'bbq-tofu
What the…??? Our lunchbox didn't change!
We said that actormaps were transactional.
We have gotten a hint of this with the above:
actormap-poke!
returns a value and commits any changes.
actormap-peek
returns a value and throws any changes away.
Actually, these two functions are just simple conveniences that wrap a
more powerful (but cumbersome to use) tool, actormap-turn
:
> (actormap-turn am lunchbox) 'bbq-tofu #<transactormap> '() '() > (actormap-turn am lunchbox 'chickpea-salad) #<transactormap> '() '()
actormap-turn
returns four values to its continuation: a return
value (if any… Racket's REPL doesn't print out a (void)
, which is
what actually got returned in the second invocation), a
"transactormap", and two lists representing messages queued for delivery.
(TODO: Currently two, one for local and one for remote vats… we are
likely to collapse down to just one list, so update this text when
that changes!)
This #<transactormap>
is interesting… we haven't mentioned this
before, but there are really two kinds of actormaps, "whactormaps"
(weak-hash actormaps) and "transactormaps" (which hold a transaction
which we can choose whether or not to commit).
Actually our am
is a whactormap:
> am
#<whactormap>
It's a weak hashtable which stores the current mutable state of all actors.
But we'd like to see about that transactormap… just running
actormap-turn
on its own won't update the lunchbox either:
> (actormap-turn am lunchbox 'chickpea-salad) #<transactormap> '() '() > (actormap-turn am lunchbox) 'bbq-tofu #<transactormap> '() '()
We need to capture the transactormap and commit it.
> (define-values (_val tr-am _tl _tr) (actormap-turn am lunchbox 'chickpea-salad)) > tr-am #<transactormap> > (transactormap-merge! tr-am)
Now at least our am
actormap should reflect that we've switched out
our 'bbq-tofu
sandwich for a 'chickpea-salad
one:
> (actormap-peek am lunchbox) 'chickpea-salad
Horray!
2.3.2 Time travel: snapshotting and restoring
But what does our actormap really look like? Right now it only has one thing in it, the lunchbox cell. We can see this by "snapshotting" the actormap.
> (snapshot-whactormap am)
'#hasheq((#<live-refr ^cell> . #<ephemeron>))
This returns an immutable actormap, frozen in time.
We can see that it only has one pairing in it, and yep… it looks
like the key is our lunchbox
reference:
> lunchbox
#<live-refr ^cell>
So it seems our "refr" is just a "reference"… it isn't the state of the object itself, it's something that points to that state, through an indirection of the actormap.
We now know that we can snapshot an actormap, we can freeze time and come back to it. First we'll freeze a snapshot of this period of time, change what's in the lunchbox, and then snapshot it again:
> (define am-snapshot1 (snapshot-whactormap am)) > (actormap-poke! am lunchbox 'sloppy-jane) ; sloppy joe w/ lentils > (define am-snapshot2 (snapshot-whactormap am))
Now let's restore them as separate whactormaps:
> (define am-restored1 (hasheq->whactormap am-snapshot1)) > (define am-restored2 (hasheq->whactormap am-snapshot2))
Now it doesn't matter what we put in our lunchbox; we can always come back to life (and lunch) as it used to be.
> (actormap-poke! am lunchbox 'peanut-butter-and-pickles) > (actormap-peek am lunchbox) 'peanut-butter-and-pickles > (actormap-peek am-restored2 lunchbox) 'sloppy-jane > (actormap-peek am-restored1 lunchbox) 'chickpea-salad
Why eat leftovers when you can simply travel back in time and eat yesterday's lunch?
2.3.3 Actors really are what they say they are
Now at last we will pull the curtain aside and see that the (hu?)man standing behind the curtain is who we thought it was all along. (This particular section is totally "optional", but hopefully illuminates some of the guts of Goblins itself.)
Here is a suitable test subject:
(define (^the-wizard bcom) ;; High and mighty (define the-wizard-behind-the-curtain (methods [(request-gift what) "HOW DARE YOU APPROACH THE ALL POWERFUL WIZARD?"] [(pull-back-curtain) (bcom the-man "Er... I can explain...")])) ;; Just a person after all (define the-man (methods [(request-gift what) (match what ['heart "Here's a clock shaped like a heart!"] ['brain "Here's a diploma!"] ['courage "The courage was inside you all along!"] [anything-else "I don't know what that is..."])] [(pull-back-curtain) "... you already pulled it back."])) ;; we start out obscured the-wizard-behind-the-curtain) (define wizard (actormap-spawn! am ^the-wizard))
We humbly approach the wizard asking for the gift of a heart:
> (actormap-peek am wizard 'request-gift 'heart) "HOW DARE YOU APPROACH THE ALL POWERFUL WIZARD?"
Is that so? Let's snapshot time both before and after we pull back the curtain so we can remember how he changes his tone:
> (define am-wizard-snapshot1 (snapshot-whactormap am)) > (actormap-poke! am wizard 'pull-back-curtain) "Er... I can explain..." > (define am-wizard-snapshot2 (snapshot-whactormap am))
Of course now, if we request a gift of the wizard, the conversation is a bit different:
> (actormap-peek am wizard 'request-gift 'heart) "Here's a clock shaped like a heart!" > (actormap-peek am wizard 'request-gift 'brain) "Here's a diploma!" > (actormap-peek am wizard 'request-gift 'courage) "The courage was inside you all along!"
Of course, this isn't too much of a surprise, since, well, we wrote the code for the wizard.
Both actormaps show our new wizard resident (and the old lunchbox cell):
> am-wizard-snapshot1 '#hasheq((#<live-refr ^the-wizard> . #<ephemeron>) (#<live-refr ^cell> . #<ephemeron>)) > am-wizard-snapshot2 '#hasheq((#<live-refr ^the-wizard> . #<ephemeron>) (#<live-refr ^cell> . #<ephemeron>))
Referencing the wizard from either hashtable gives us this weird-looking ephemeron thing:
> (hash-ref am-wizard-snapshot1 wizard) #<ephemeron>
Well, ephemerons are just a datastructure to help the garbage collection in the weak hashtable of whactormaps work right. So we can peel that off:
> (ephemeron-value (hash-ref am-wizard-snapshot1 wizard)) #<mactor:local-actor>
What's a… "mactor"?
Well, it stands for "meta-actor"… there are a few of these
kinds of things.
A mactor:local-actor
is one kind of mactor (actually the most common
kind), one that works in the usual way of wrapping a procedure.
It would be wonderful to be able to look at that procedure. Unfortunately, we haven't pulled in all the tools to do it yet, but there is a way to do so:
> (require (submod goblins/core mactor-extra))
This gives us access to mactor:local-actor-handler
, which is what we
want.
> (mactor:local-actor-handler (ephemeron-value (hash-ref am-wizard-snapshot1 wizard))) #<procedure:methods>
Let's give names to both snapshotted wizard handlers:
> (define wizard-handler1 (mactor:local-actor-handler (ephemeron-value (hash-ref am-wizard-snapshot1 wizard)))) > (define wizard-handler2 (mactor:local-actor-handler (ephemeron-value (hash-ref am-wizard-snapshot2 wizard))))
Since these are just procedures, we can try calling them directly. As we can see, the first wizard is petulant in response to our request for a gift, whereas the second wizard, having had the curtain pulled away, is happy to assist.
> (wizard-handler1 'request-gift 'heart) "HOW DARE YOU APPROACH THE ALL POWERFUL WIZARD?" > (wizard-handler2 'request-gift 'heart) "Here's a clock shaped like a heart!"
So this really was the procedure we thought it was!
But wait… if we remember correctly, our first wizard version,
the-wizard-behind-the-curtain
, returned its request to "become"
something (as well as its return value) wrapped by its bcom
capability when we called the 'pull-back-curtain
method.
So what happens if we try calling that again on the unwrapped
procedure?
> (wizard-handler1 'pull-back-curtain) #<become>
That's interesting… it returned a "become" object! How can we unwrap it?
Well, here's the secret about bcom
: as we said, it is a capability,
but it's actually using a "sealer" (we'll get to what those are later,
which as it sounds like is a kind of capability to "seal" an object in
a certain way.
To unseal it, we need the corresponding "unsealer".
mactor:local-actor
has a way of detecting that the value is wrapped
in this sealer, as well as a way of unsealing it:
> (define wizard-become? (mactor:local-actor-become? (ephemeron-value (hash-ref am-wizard-snapshot1 wizard)))) > (define wizard-become-unsealer (mactor:local-actor-become-unsealer (ephemeron-value (hash-ref am-wizard-snapshot1 wizard))))
Now we can see that the value returned from the 'pull-back-curtain
method is from the wizard's bcom
sealer:
> (wizard-become? (wizard-handler1 'pull-back-curtain)) #t
And now, at last! We can peer into its contents:
> (wizard-become-unsealer (wizard-handler1 'pull-back-curtain)) #<procedure:methods> "Er... I can explain..."
Aha!
It looks like it returns two values to its continuation…
the procedure that the actor would like to become, as well as
an extra (and optional, defaulting to (void)
) return value.
Let's bind the first of those to a useful name:
> (define-values (new-wizard-handler _wizval) (wizard-become-unsealer (wizard-handler1 'pull-back-curtain)))
Now as you've probably guessed, this ought to be the same procedure as
wizard-handler2
.
Is it?
;; Works just like wizard-handler2... > (new-wizard-handler 'request-gift 'courage) "The courage was inside you all along!" ;; ... because it *is* wizard-handler2! > (eq? new-wizard-handler wizard-handler2) #t
Woohoo!
Of course, the tools we've seen in this subsection are not really tools that we expect users of Goblins to use very regularly. (Indeed, when we implement better module-level security, rarely will they have access to them.) But now we've gotten a peek inside of Goblins' machinery, and seen that it was mostly what we expected all along.
I feel like all that learning is was worth a reward, don't you?
> (new-wizard-handler 'request-gift 'brain) "Here's a diploma!"
Yes, we deserve it!
2.3.4 So how does message passing work?
TODO: write this
2.3.5 How can two actormaps communicate? (How do vats do it?)
TODO: write this
2.4 Advanced object patterns
TODO: write this
2.5 How this relates to "object capability security"
TODO: write this
2.6 Sealers/unsealers and rights amplification
TODO: write this
2.7 Networked object interactions
Big ol' TODO here since we need to implement it, too!
3 Core API
4 Actormap API
5 Vat API
6 Distributed Goblins
TODO