(channels '("##botchat")))
(define hive (make-hive))
(define irc-bot
- (hive-create-actor* hive <my-irc-bot> "irc-bot"
- #:username username
- #:server server
- #:channels channels))
+ (bootstrap-actor* hive <my-irc-bot> "irc-bot"
+ #:username username
+ #:server server
+ #:channels channels))
(run-hive hive (list (bootstrap-message hive irc-bot 'init))))
#+END_SRC
Actors are connected to something called a "hive", which is a
special kind of actor that runs all the other actors.
Actors can spawn other actors, but before we start the hive we use
-this special "hive-create-actor*" method.
+this special "bootstrap-actor*" method.
It takes the hive as its first argument, the actor class as the second
-argument, a decoraive "cookie" as the third argument (this is
+argument, a decorative "cookie" as the third argument (this is
optional, but it helps with debugging... you can skip it by setting it
to #f if you prefer), and the rest are initialization arguments to the
-actor. hive-create-actor* passes back not the actor itself (we don't
+actor. bootstrap-actor* passes back not the actor itself (we don't
get access to that usually) but the *id* of the actor.
(More on this later.)
Finally we run the hive with run-hive and pass it a list of
#+BEGIN_SRC scheme
(define-method (handle-line (irc-bot <my-irc-bot>) speaker channel
line emote?)
- (<- irc-bot (actor-id irc-bot) 'send-line channel
+ (<- (actor-id irc-bot) 'send-line channel
(format #f "Bawwwwk! ~a says: ~a" speaker line)))
#+END_SRC
(or (equal? str my-name)
(equal? str (string-concatenate (list my-name ":")))))
(when (looks-like-me?)
- (<- irc-bot (actor-id irc-bot) 'send-line channel
+ (<- (actor-id irc-bot) 'send-line channel
(format #f "Bawwwwk! ~a says: ~a" speaker line))))
#+END_SRC
(match action
;; The classic botsnack!
("botsnack"
- (<- irc-bot (actor-id irc-bot) 'send-line channel
+ (<- (actor-id irc-bot) 'send-line channel
"Yippie! *does a dance!*"))
;; Return greeting
((or "hello" "hello!" "hello." "greetings" "greetings." "greetings!"
"hei" "hei." "hei!" "hi" "hi!")
- (<- irc-bot (actor-id irc-bot) 'send-line channel
+ (<- (actor-id irc-bot) 'send-line channel
(format #f "Oh hi ~a!" speaker)))
("echo"
- (<- irc-bot (actor-id irc-bot) 'send-line channel
+ (<- (actor-id irc-bot) 'send-line channel
(string-join action-args " ")))
;; ---> Add yours here <---
;; Default
(_
- (<- irc-bot (actor-id irc-bot) 'send-line channel
- "*stupid puppy look*"))))
- ;; Otherwise... just spit the output to current-output-port or whatever
- (_
- (if emote?
- (format #t "~a emoted ~s in channel ~a\n"
- speaker line channel)
- (format #t "~a said ~s in channel ~a\n"
- speaker line channel)))))
+ (<- (actor-id irc-bot) 'send-line channel
+ "*stupid puppy look*"))))))
#+END_SRC
Parsing the pattern matcher syntax is left as an exercise for the
(or (equal? str my-name)
(equal? str (string-concatenate (list my-name ":")))))
(define (respond respond-line)
- (<- irc-bot (actor-id irc-bot) 'send-line channel
+ (<- (actor-id irc-bot) 'send-line channel
respond-line))
(match (string-split line #\space)
(((? looks-like-me? _) action action-args ...)
;; Default
(_
- (respond "*stupid puppy look*"))))
- ;; Otherwise... just spit the output to current-output-port or whatever
- (_
- (if emote?
- (format #t "~a emoted ~s in channel ~a\n"
- speaker line channel)
- (format #t "~a said ~s in channel ~a\n"
- speaker line channel)))))
+ (respond "*stupid puppy look*"))))))
#+END_SRC
Okay, that looks pretty good!
** An intermission: about live hacking
-** Battle bot!
+This section is optional, but highly recommended.
+It requires that you're a user of GNU Emacs.
+If you aren't, don't worry... you can forge ahead and come back in case
+you ever do become an Emacs user.
+(If you're more familiar with Vi/Vim style editing, I hear good things
+about Spacemacs...)
+
+So you may have noticed while updating the last section that the
+start/stop cycle of hacking isn't really ideal.
+You might either edit a file in your editor, then run it, or
+type the whole program into the REPL, but then you'll have to spend
+extra time copying it to a file.
+Wouldn't it be nice if it were possible to both write code in a
+file and try it as you go?
+And wouldn't it be even better if you could live edit a program
+while it's running?
+
+Luckily, there's a great Emacs mode called Geiser which makes
+editing and hacking and experimenting all happen in harmony.
+And even better, 8sync is optimized for this experience.
+8sync provides easy drop-in "cooperative REPL" support, and
+most code can be simply redefined on the fly in 8sync through Geiser
+and actors will immediately update their behavior, so you can test
+and tweak things as you go.
+
+Okay, enough talking. Let's add it!
+Redefine run-bot like so:
+
+#+BEGIN_SRC scheme
+ (define* (run-bot #:key (username "examplebot")
+ (server "irc.freenode.net")
+ (channels '("##botchat"))
+ (repl-path "/tmp/8sync-repl"))
+ (define hive (make-hive))
+ (define irc-bot
+ (bootstrap-actor* hive <my-irc-bot> "irc-bot"
+ #:username username
+ #:server server
+ #:channels channels))
+ (define repl-manager
+ (bootstrap-actor* hive <repl-manager> "repl"
+ #:path repl-path))
+
+ (run-hive hive (list (bootstrap-message hive irc-bot 'init)
+ (bootstrap-message hive repl-manager 'init))))
+#+END_SRC
+
+If we put a call to run-bot at the bottom of our file we can call it,
+and the repl-manager will start something we can connect to automatically.
+Horray!
+Now when we run this it'll start up a REPL with a unix domain socket at
+the repl-path.
+We can connect to it in emacs like so:
+
+: M-x geiser-connect-local <RET> guile <RET> /tmp/8sync-repl <RET>
+
+Okay, so what does this get us?
+Well, we can now live edit our program.
+Let's change how our bot behaves a bit.
+Let's change handle-line and tweak how the bot responds to a botsnack.
+Change this part:
+
+#+BEGIN_SRC scheme
+ ;; From this:
+ ("botsnack"
+ (respond "Yippie! *does a dance!*"))
+
+ ;; To this:
+ ("botsnack"
+ (respond "Yippie! *catches botsnack in midair!*"))
+#+END_SRC
+
+Okay, now let's evaluate the change of the definition.
+You can hit "C-M-x" anywhere in the definition to re-evaluate.
+(You can also position your cursor at the end of the definition and press
+"C-x C-e", but I've come to like "C-M-x" better because I can evaluate as soon
+as I'm done writing.)
+Now, on IRC, ask your bot for a botsnack.
+The bot should give the new message... with no need to stop and start the
+program!
+
+Let's fix a bug live.
+Our current program works great if you talk to your bot in the same
+IRC channel, but what if you try to talk to them over private message?
+
+#+BEGIN_SRC text
+IRC> /query examplebot
+<foo-user> examplebot: hi!
+#+END_SRC
+
+Hm, we aren't seeing any response on IRC!
+Huh? What's going on?
+It's time to do some debugging.
+There are plenty of debugging tools in Guile, but sometimes the simplest
+is the nicest, and the simplest debugging route around is good old
+fashioned print debugging.
+
+It turns out Guile has an under-advertised feature which makes print
+debugging really easy called "pk", pronounced "peek".
+What pk accepts a list of arguments, prints out the whole thing,
+but returns the last argument.
+This makes wrapping bits of our code pretty easy to see what's
+going on.
+So let's peek into our program with pk.
+Edit the respond section to see what channel it's really sending
+things to:
+
+#+BEGIN_SRC scheme
+ (define-method (handle-line (irc-bot <my-irc-bot>) speaker channel
+ line emote?)
+ ;; [... snip ...]
+ (define (respond respond-line)
+ (<- (actor-id irc-bot) 'send-line (pk 'channel channel)
+ respond-line))
+ ;; [... snip ...]
+ )
+#+END_SRC
+
+Re-evaluate.
+Now let's ping our bot in both the channel and over PM.
+
+#+BEGIN_SRC text
+;;; (channel "##botchat")
+
+;;; (channel "sinkbot")
+#+END_SRC
+
+Oh okay, this makes sense.
+When we're talking in a normal multi-user channel, the channel we see
+the message coming from is the same one we send to.
+But over PM, the channel is a username, and in this case the username
+we're sending our line of text to is ourselves.
+That isn't what we want.
+Let's edit our code so that if we see that the channel we're sending
+to looks like our own username that we respond back to the sender.
+(We can remove the pk now that we know what's going on.)
+
+#+BEGIN_SRC scheme
+ (define-method (handle-line (irc-bot <my-irc-bot>) speaker channel
+ line emote?)
+ ;; [... snip ...]
+ (define (respond respond-line)
+ (<- (actor-id irc-bot) 'send-line
+ (if (looks-like-me? channel)
+ speaker ; PM session
+ channel) ; normal IRC channel
+ respond-line))
+ ;; [... snip ...]
+ )
+#+END_SRC
+
+Re-evaluate and test.
+
+#+BEGIN_SRC text
+IRC> /query examplebot
+<foo-user> examplebot: hi!
+<examplebot> Oh hi foo-user!
+#+END_SRC
+
+Horray!
-** Adding a "rankings" web page
+** Writing our own actors
+
+Let's write the most basic, boring actor possible.
+How about an actor that start sleeping, and keeps sleeping?
+
+#+BEGIN_SRC scheme
+ (use-modules (oop goops)
+ (8sync))
+
+ (define-class <sleeper> (<actor>)
+ (actions #:allocation #:each-subclass
+ #:init-value (build-actions
+ (loop sleeper-loop))))
+
+ (define (sleeper-loop actor message)
+ (while (actor-alive? actor)
+ (display "Zzzzzzzz....\n")
+ ;; Sleep for one second
+ (8sleep 1)))
+
+ (let* ((hive (make-hive))
+ (sleeper (bootstrap-actor hive <sleeper>)))
+ (run-hive hive (list (bootstrap-message hive sleeper 'loop))))
+#+END_SRC
+
+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.
+(#:class should be fine, except there is [[https://debbugs.gnu.org/cgi/bugreport.cgi?bug=25211][a bug in Guile]] which keeps
+us from using it for now.)
+
+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.
+(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.)
+
+This actor is pretty lazy though.
+Time to get back to work!
+
+#+BEGIN_SRC scheme
+ (use-modules (8sync)
+ (oop goops))
+
+ (define-class <manager> (<actor>)
+ (direct-report #:init-keyword #:direct-report
+ #:getter manager-direct-report)
+ (actions #:allocation #:each-subclass
+ #:init-value (build-actions
+ (assign-task manager-assign-task))))
+
+ (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))
+#+END_SRC
+
+Here we're constructing a very simple manager actor.
+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.
+
+#+BEGIN_SRC scheme
+ (define-class <worker> (<actor>)
+ (task-left #:init-keyword #:task-left
+ #:accessor worker-task-left)
+ (actions #:allocation #:each-subclass
+ #:init-value (build-actions
+ (work-on-this worker-work-on-this))))
+
+ (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))))
+#+END_SRC
+
+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:
+
+#+BEGIN_SRC scheme
+ (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))))
+#+END_SRC
+
+#+BEGIN_SRC text
+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*
+#+END_SRC
+
+"<-" 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.
+
+#+END_SRC
+#+BEGIN_SRC scheme
+ ;;; 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))))))
+#+END_SRC
+
+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.
+
+#+BEGIN_SRC scheme
+ ;;; Update the worker to add the following new actions:
+ (define-class <worker> (<actor>)
+ (task-left #:init-keyword #:task-left
+ #:accessor worker-task-left)
+ (actions #:allocation #:each-subclass
+ #:init-value (build-actions
+ (work-on-this worker-work-on-this)
+ ;; Add these:
+ (done-yet? worker-done-yet?)
+ (go-home worker-go-home))))
+
+ ;;; 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))
+#+END_SRC
+
+(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.)
+
+Running it is the same as before:
+
+#+BEGIN_SRC scheme
+ (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))))
+#+END_SRC
+
+But the output is a bit different:
+
+#+BEGIN_SRC scheme
+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.
+#+END_SRC
+
+"<-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!
** Writing our own <irc-bot> from scratch
* API reference
+* Systems reference
+** IRC
+** Web / HTTP
+** COMMENT Websockets
+
+* Addendum
+** Recommended .emacs additions
+
+In order for =mbody-receive= to indent properly, put this in your
+.emacs:
+
+#+BEGIN_SRC emacs-lisp
+(put 'mbody-receive 'scheme-indent-function 2)
+#+END_SRC
+
+** 8sync and Fibers
+
+One other major library for asynchronous communication in Guile-land
+is [[https://github.com/wingo/fibers/][Fibers]].
+There's a lot of overlap:
+
+ - Both use Guile's suspendable-ports facility
+ - Both communicate between asynchronous processes using message passing;
+ you don't have to squint hard to see the relationship between Fibers'
+ channels and 8sync's actor inboxes.
+
+However, there are clearly differences too.
+There's a one to one relationship between 8sync actors and an actor inbox,
+whereas each Fibers fiber may read from multiple channels, for example.
+
+Luckily, it turns out there's a clear relationship, based on real,
+actual theory!
+8sync is based on the [[https://en.wikipedia.org/wiki/Actor_model][actor model]] whereas fibers follows
+[[http://usingcsp.com/][Communicating Sequential Processes (CSP)]], which is a form of
+[[https://en.wikipedia.org/wiki/Process_calculus][process calculi]].
+And it turns out, the
+[[https://en.wikipedia.org/wiki/Actor_model_and_process_calculi][relationship between the actor model and process calculi]] is well documented,
+and even more precisely, the
+[[https://en.wikipedia.org/wiki/Communicating_sequential_processes#Comparison_with_the_Actor_Model][relationship between CSP and the actor model]] is well understood too.
+
+So, 8sync and Fibers do take somewhat different approaches, but both
+have a solid theoretical backing... and their theories are well
+understood in terms of each other.
+Good news for theory nerds!
+
+(Since the actors and CSP are [[https://en.wikipedia.org/wiki/Dual_%28mathematics%29][dual]], maybe eventually 8sync will be
+implemented on top of Fibers... that remains to be seen!)
+