Next: , Previous: , Up: Tutorial   [Contents][Index]


2.2 Writing our own actors

Let’s write the most basic, boring actor possible. How about an actor that start sleeping, and keeps sleeping?

(use-modules (oop goops)
             (8sync))

(define-class <sleeper> (<actor>)
  (actions #:allocation #:each-subclass
           #:init-value (build-actions
                         (*init* sleeper-loop))))

(define (sleeper-loop actor message)
  (while (actor-alive? actor)
    (display "Zzzzzzzz....\n")
    ;; Sleep for one second
    (8sleep (sleeper-sleep-secs actor))))

(let* ((hive (make-hive))
       (sleeper (bootstrap-actor hive <sleeper>)))
  (run-hive hive '()))

We see some particular things in this example. One thing is that our <sleeper> actor has an actions slot. This is used to look up what the "action handler" for a message is. We have to set the #:allocation to either #:each-subclass or #:class.3

The only action handler we’ve added is for *init*, which is called implicitly when the actor first starts up. (This will be true whether we bootstrap the actor before the hive starts or create it during the hive’s execution.)

In our sleeper-loop we also see a call to "8sleep". "8sleep" is like Guile’s "sleep" method, except it is non-blocking and will always yield to the scheduler.

Our while loop also checks "actor-alive?" to see whether or not it is still registered. In general, if you keep a loop in your actor that regularly yields to the scheduler, you should check this.4 (An alternate way to handle it would be to not use a while loop at all but simply send a message to ourselves with "<-" to call the sleeper-loop handler again. If the actor was dead, the message simply would not be delivered and thus the loop would stop.)

It turns out we could have written the class for the actor much more simply:

;; You could do this instead of the define-class above.
(define-actor <sleeper> (<actor>)
  ((*init* sleeper-loop)))

This is sugar, and expands into exactly the same thing as the define-class above. The third argument is an argument list, the same as what’s passed into build-actions. Everything after that is a slot. So for example, if we had added an optional slot to specify how many seconds to sleep, we could have done it like so:

(define-actor <sleeper> (<actor>)
  ((*init* sleeper-loop))
  (sleep-secs #:init-value 1
              #:getter sleeper-sleep-secs))

This actor is pretty lazy though. Time to get back to work! Let’s build a worker / manager type system.

(use-modules (8sync)
             (oop goops))

(define-actor <manager> (<actor>)
  ((assign-task manager-assign-task))
  (direct-report #:init-keyword #:direct-report
                 #:getter manager-direct-report))

(define (manager-assign-task manager message difficulty)
  "Delegate a task to our direct report"
  (display "manager> Work on this task for me!\n")
  (<- (manager-direct-report manager)
      'work-on-this difficulty))

This manager keeps track of a direct report and tells them to start working on a task… simple delegation. Nothing here is really new, but note that our friend "<-" (which means "send message") is back. There’s one difference this time… the first time we saw "<-" was in the handle-line procedure of the irc-bot, and in that case we explicitly pulled the actor-id after the actor we were sending the message to (ourselves), which we aren’t doing here. But that was an unusual case, because the actor was ourself. In this case, and in general, actors don’t have direct references to other actors; instead, all they have is access to identifiers which reference other actors.

(define-actor <worker> (<actor>)
  ((work-on-this worker-work-on-this))
  (task-left #:init-keyword #:task-left
             #:accessor worker-task-left))

(define (worker-work-on-this worker message difficulty)
  "Work on one task until done."
  (set! (worker-task-left worker) difficulty)
  (display "worker> Whatever you say, boss!\n")
  (while (and (actor-alive? worker)
              (> (worker-task-left worker) 0))
    (display "worker> *huff puff*\n")
    (set! (worker-task-left worker)
          (- (worker-task-left worker) 1))
    (8sleep (/ 1 3))))

The worker also contains familiar code, but we now see that we can call 8sleep with non-integer real numbers.

Looks like there’s nothing left to do but run it.

(let* ((hive (make-hive))
       (worker (bootstrap-actor hive <worker>))
       (manager (bootstrap-actor hive <manager>
                                 #:direct-report worker)))
  (run-hive hive (list (bootstrap-message hive manager 'assign-task 5))))

Unlike the <sleeper>, our <manager> doesn’t have an implicit *init* method, so we’ve bootstrapped the calling assign-task action.

manager> Work on this task for me!
worker> Whatever you say, boss!
worker> *huff puff*
worker> *huff puff*
worker> *huff puff*
worker> *huff puff*
worker> *huff puff*

"<-" pays no attention to what happens with the messages it has sent off. This is useful in many cases… we can blast off many messages and continue along without holding anything back.

But sometimes we want to make sure that something completes before we do something else, or we want to send a message and get some sort of information back. Luckily 8sync comes with an answer to that with "<-wait", which will suspend the caller until the callee gives some sort of response, but which does not block the rest of the program from running. Let’s try applying that to our own code by turning our manager into a micromanager.

;;; Update this method
(define (manager-assign-task manager message difficulty)
  "Delegate a task to our direct report"
  (display "manager> Work on this task for me!\n")
  (<- (manager-direct-report manager)
      'work-on-this difficulty)

  ;; Wait a moment, then call the micromanagement loop
  (8sleep (/ 1 2))
  (manager-micromanage-loop manager))

;;; And add the following
;;;   (... Note: do not model actual employee management off this)
(define (manager-micromanage-loop manager)
  "Pester direct report until they're done with their task."
  (display "manager> Are you done yet???\n")
  (let ((worker-is-done
         (mbody-val (<-wait (manager-direct-report manager)
                            'done-yet?))))
    (if worker-is-done
        (begin (display "manager> Oh!  I guess you can go home then.\n")
               (<- (manager-direct-report manager) 'go-home))
        (begin (display "manager> Harumph!\n")
               (8sleep (/ 1 2))
               (when (actor-alive? manager)
                 (manager-micromanage-loop manager))))))

We’ve appended a micromanagement loop here… but what’s going on? "<-wait", as it sounds, waits for a reply, and returns a reply message. In this case there’s a value in the body of the message we want, so we pull it out with mbody-val. (It’s possible for a remote actor to return multiple values, in which case we’d want to use mbody-receive, but that’s a bit more complicated.)

Of course, we need to update our worker accordingly as well.

;;; Update the worker to add the following new actions:
(define-actor <worker> (<actor>)
  ((work-on-this worker-work-on-this)
   ;; Add these:
   (done-yet? worker-done-yet?)
   (go-home worker-go-home))
  (task-left #:init-keyword #:task-left
             #:accessor worker-task-left))

;;; New procedures:
(define (worker-done-yet? worker message)
  "Reply with whether or not we're done yet."
  (let ((am-i-done? (= (worker-task-left worker) 0)))
    (if am-i-done?
        (display "worker> Yes, I finished up!\n")
        (display "worker> No... I'm still working on it...\n"))
    (<-reply message am-i-done?)))

(define (worker-go-home worker message)
  "It's off of work for us!"
  (display "worker> Whew!  Free at last.\n")
  (self-destruct worker))

(As you’ve probably guessed, you wouldn’t normally call display everywhere as we are in this program… that’s just to make the examples more illustrative.)

"<-reply" is what actually returns the information to the actor waiting on the reply. It takes as an argument the actor sending the message, the message it is in reply to, and the rest of the arguments are the "body" of the message. (If an actor handles a message that is being "waited on" but does not explicitly reply to it, an auto-reply with an empty body will be triggered so that the waiting actor is not left waiting around.)

The last thing to note is the call to "self-destruct". This does what you might expect: it removes the actor from the hive. No new messages will be sent to it. Ka-poof!

Running it is the same as before:

(let* ((hive (make-hive))
       (worker (bootstrap-actor hive <worker>))
       (manager (bootstrap-actor hive <manager>
                                 #:direct-report worker)))
  (run-hive hive (list (bootstrap-message hive manager 'assign-task 5))))

But the output is a bit different:

manager> Work on this task for me!
worker> Whatever you say, boss!
worker> *huff puff*
worker> *huff puff*
manager> Are you done yet???
worker> No... I'm still working on it...
manager> Harumph!
worker> *huff puff*
manager> Are you done yet???
worker> *huff puff*
worker> No... I'm still working on it...
manager> Harumph!
worker> *huff puff*
manager> Are you done yet???
worker> Yes, I finished up!
manager> Oh!  I guess you can go home then.
worker> Whew!  Free at last.

Footnotes

(3)

#:class should be fine, except there is a bug in Guile which keeps us from using it for now.

(4)

Or rather, for now you should call actor-alive? if your code is looping like this. In the future, after an actor dies, its coroutines will automatically be "canceled".


Next: , Previous: , Up: Tutorial   [Contents][Index]