Next: Synchronous calls, Up: Objects [Contents][Index]
Goblins objects are fundamentally just procedures wrapped in a
constructor definition. Any procedure accepting at least one argument
(bcom
) and returning another procedure (the behavior) works as an
object constructor, like this one:
(define (^greeter bcom our-name) (lambda (your-name) (string-append "Hello, " your-name "!")))
In some cases, manually defining objects like this may be necessary.
However, in the general case, the recommended approach is to use the
define-actor
macro, which would look like this:
(define-actor (^greeter bcom our-name) (lambda (your-name) (string-append "Hello, " your-name "!")))
define-actor
simplifies common patterns like persistence
(see Persistence) and makes live hacking more convenient:
Define a Goblins object with object-name. The argument syntax
accepts the #:key
and #:optional
keywords from
lambda*
and define*
. By default, define-actor
returns a redefinable object to aid in live hacking.
If provided, the argument to #:portrait
must be a procedure that
accepts no arguments (a thunk). This thunk is called in the context of
the define-actor
body and has access to any arguments or
definitions available there. Its return value must be a list or
versioned
object (see Upgrading through persistence)
containing arguments, in order, for the restoration procedure. If the
return value is a versioned
object and the #:versioned
argument is provided, the two versions must be equal?
. If
#:portrait
is not provided, the arguments to the constructor are
used as the portrait data.
If provided, the argument to #:version
is used to tag portrait
data. If the portrait procedure returns a versioned
object, the
two versions must be equal?
. If no #:version
argument is
provided and the portrait procedure does not return a versioned
object, the portrait data will be unversioned; its version will be
treated as 0
on restoration.
If provided, the argument to #:restore
must be a procedure that
accepts at least two arguments: the version of portrait data, and the
portrait data proper. This procedure is applied to the version and the
portrait data. If no #:restore
procedure is provided, during
restoration, the constructor is applied directly to the portrait data
without the version.
If provided, the argument to #:upgrade
must be a procedure
accepting arguments of the same kind as #:restore
. It should
return two values: the new version, and the new portrait data as a list.
If #:restore
is also provided, it is applied to the resulting
values; if it is not, the constructor is applied to the new portrait
data. While #:upgrade
is designed for use with
migrations
, it is generic enough to be used on its own.
If provided, the argument to #:self
must be an identifier (not a
symbol) which is bound to the object’s refr inside the object body.
This allows the object to send messages to itself at runtime (but not
construction time).
If provided, the #:frozen
flag indicates that the object will not
be redefinable. This provides a slight performance improvement to
spawning (see spawn) at the cost of less convenient live hacking.
#:frozen
cannot be used with #:restore
.
Here is a very simple example defining a cell object. Its portrait
would be (list val)
:
(define-actor (^cell bcom #:optional val) (case-lambda ;; Called with no arguments; return the current value [() val] ;; Called with one argument; become a version with this new value [(new-val) (bcom (^cell bcom new-val))]))
Here’s a more complex example. This code shows a thermostat object
which is constructed with two arguments: a cell holding the temperature,
and a reference to a heater object. It defines a portrait procedure
which extracts the value from the cell, serializing the value in the
cell rather than the cell reference. It specifies a version of 1
so it can upgrade from a prior version. In the restore function, it
matches on the version, converting from the original Celsius value to
the new Kelvin value if necessary; then spawns ^thermostat
with a
new cell holding the temperature and the same reference to the heater:
(define-actor (^thermostat bcom set-temp-cell heater-refr) #:portrait (lambda () (list ($ set-temp-cell) heater-refr)) #:version 1 #:restore (lambda (version set-temp heater-refr) ;; Version 0 used C; convert to Kelvin (define maybe-converted-temp (match version [0 (+ set-temp 273.15)] [1 set-temp])) (spawn ^thermostat (spawn ^cell maybe-converted-temp) heater-refr)) (methods [(set-temp new-temp) ($ set-temp-cell new-temp)] ;; Invoked with current room temp [(tick current-temp) (if (< current-temp ($ set-temp-cell)) (<-np heater-refr 'turn-on) (<-np heater-refr 'turn-off))]))
This actor could very well have kept the temperature in the cell; there’s usually no reason to unpack values like this. However, unpacking the value demonstrates that code can be called inside the portrait thunk, and that the portrait data can be transformed in the portraitization and restoration process.
Sometimes it’s convenient for an actor to be able to refer to itself. Take the example of a deck of playing cards using its own method within another method:
(define-actor (^deck bcom #:optional crds) #:self self ;; define helpers etc. here... (define-cell cards (or crds (init-deck))) (methods ... ((top) (car ($ cards))) ((draw) (let ((top ($ self 'top))) ($ cards (cdr ($ cards))) top)) ...))
Once you have a constructor, you can use it to create live objects with
the spawn
operator:
Construct and return a reference to a new object.
bcom
, which can be used to change the object’s behavior.
For example, given the following constructor:
(define* (^cell bcom #:optional val) ; constructor (outer procedure) (case-lambda ; behavior (inner procedure) (() ; 0-argument invocation (getter) val) ; (return current value) ((new-val) ; 1-argument invocation (setter) (bcom (^cell bcom new-val))))) ; ("become" ^cell with new value)
The instantiation…
(define some-cell (spawn ^cell 42))
will bind val
to 42
.
The bcom
argument warrants further explanation. It is a
capability which allows the actor to change its own behavior. It must
be called in a tail position, since bcom
returns a
data structure demonstrating that this object is choosing to change its
value (part of Goblins’ quasi-functional design).
bcom
can also take a second argument which will be returned
from the object’s invocation as a return value. This allows an object
to both change its behavior and return a value with the same
invocation.
The following actor represents a consumable object that returns a string explaining its current status:
(define (^drink bcom drink-name portions) (define (drinkable-beh portions) ; "-beh" is a common suffix for "-behavior" (lambda () (define portions-left (- portions 1)) (if (zero? portions-left) (bcom empty-beh (format #f "*GLUG!* You drink your ~a. It's empty!" drink-name)) (bcom (drinkable-beh portions-left) (format #f "*GLUG!* You drink your ~a. Still some left!" drink-name))))) (define (empty-beh) "Sadly, your glass appears to be empty!") (drinkable-beh portions))
Here’s how this works at the REPL:
> (define a-vat (spawn-vat)) > ,enter-vat a-vat > (define root-beer (spawn ^drink "root beer" 3)) > ($ root-beer) => "*GLUG!* You drink your root beer. Still some left!" > ($ root-beer) => "*GLUG!* You drink your root beer. Still some left!" > ($ root-beer) => "*GLUG!* You drink your root beer. It's empty!" > ($ root-beer) => "Sadly, your glass appears to be empty!"
As you can see above, bcom
is also a handy way to construct
state machines, as ^drink
moves from drinkable to empty as it
is consumed.
spawn
also has specialized variants:
Like spawn
, and set the object’s debug name to name.
Next: Synchronous calls, Up: Objects [Contents][Index]