Next: , Up: Objects   [Contents][Index]


5.1.1 Object definition

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:

Syntax: define-actor (object-name var … [#:optional vardef …] [#:key vardef …]) [#:restore procedure] [#:portrait thunk] [#:version version] [#:frozen] [#:upgrade migrator] [#:self name] body …

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:

Procedure: spawn constructor args …

Construct and return a reference to a new object.

  • constructor: Procedure which returns a procedure representing the object’s initial behavior; implicitly passed a first argument, bcom, which can be used to change the object’s behavior.
  • args: Remaining arguments passed to constructor.

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:

Procedure: spawn-named name constructor args …

Like spawn, and set the object’s debug name to name.


Next: Synchronous calls, Up: Objects   [Contents][Index]