Basic 8sync tutorial: Hello, cats!

Table of Contents

Welcome to 8sync! This tutorial introduces you to the basics of the 8sync agenda. We'll be keeping it simple in this version and will use timing to demonstrate the agenda's basic features. (Asynchronous IO functionality, which is more useful but a bit more complex, will follow in the next tutorial.)

You can enter this all in at a REPL, or type it all into a file and then run it like:

# to load the file and then play around
guile -l hello-cats.scm

# to just run (run-demo)
guile -l hello-cats.scm -c "(run-demo)"

… or use emacs + geiser to do a combination of editing and playing at the REPL (this is the best option, but if you don't know those tools, you can come back to them later if you like).

With that said, let's jump in!

1 Hello zzzzz!

Before we can do anything, we need to import the 8sync agenda.

(use-modules (8sync agenda))

We want to do something… well, why not a hello world? Well, you've probably done a lot of those, and they aren't the most exciting part of a tutorial, so we'll reflect that with hello-snore instead.

(define (hello-snore)
  (display "Zzzzzzzz....\n"))

To start this up, we need to make an agenda with hello-snore added to the initial queue and initialize it.

(define (run-demo)
  (start-agenda
   (make-agenda #:queue
                (make-q* hello-snore))))

You should see a snore emitted to your terminal, and then the agenda will finish.

scheme@(guile-user)> (run-demo)
Zzzzzzzz....
$91 = done

We could make this snore forever… we could return a "run-request" to run the procedure again in 1 second by using run-delay:

(define (hello-snore)
  (display "Zzzzzzzz....\n")
  (run-delay (hello-snore) 1))

Now when we run it, it keeps snoring:

scheme@(guile-user)> (run-demo)
Zzzzzzzz....
Zzzzzzzz....
Zzzzzzzz....

Press Ctrl-C to interrupt the snoring (and if at the Guile REPL, you might want to type ",q" to quit the debugging prompt).

We could have used "run" instead of "run-delay", but this would have quickly filled your terminal with snoring!

We can call hello-snore on its own to see what that run-delay is doing. We said it was creating a run-request, and it is:

scheme@(guile-user)> (hello-snore)
Zzzzzzzz....
$3 = #<<run-request> proc: #<procedure 1d08640 at socket:20:2 ()> when: #<<time-delta> sec: 1 usec: 0>>

We can see now how the looping works: the agenda simply runs procedures and watches to see if they return run-request (or, as we will see in the future, port-request) calls. run, run-delay and friends are sugar; we could easily have returned our own run-request:

(define (hello-snore)
  (display "Zzzzzzzz....\n")
  (make-run-request hello-snore
                    (make-time-delta 1)))

But as you can see, the run-delay looks much nicer. Another nicer thing is that run, run-delay and friends let you write code as if you were invoking the function itself, by wrapping your code for you:

(define (snore-with-sheep sheep-count)
  (format #t "ZZzzzzz... ~a sheep...\n"
          sheep-count)
  (run-delay (snore-with-sheep (+ sheep-count 1))
             1))

(define (run-demo)
  (start-agenda
   (make-agenda #:queue
                (make-q* (wrap (snore-with-sheep 1))))))
scheme@(guile-user)> (run-demo)
ZZzzzzz... 1 sheep...
ZZzzzzz... 2 sheep...
ZZzzzzz... 3 sheep...
ZZzzzzz... 4 sheep...

You may notice the use of (wrap), which very simply creates a "closure" at point for you… and indeed, this is all run and run-delay are doing, wrapping the stuff you want to call up into a closure and dumping it in a run-request. It's pretty simple!

(The indirection of wrapping things in a closure turns out to have an added benefit of being easier to modify your code when live hacking, but we'll address that in another chapter.)

2 The cat chorus

That's enough sheep counting. We don't want to put you, the reader, to sleep! 8sync advertises itself as a tool for building all these fancy internet-oriented applications and stuff, and we all know the internet is made out of cats, so let's build some cats.

(use-modules (oop goops))

(define-class <cat> ()
  ;; Every cat needs a name.
  (name #:init-keyword #:name)
  ;; What kind of noise we make when we "sing"
  (noise #:init-value "Meow"
         #:init-keyword #:noise)
  ;; How many seconds until we sing next
  (sing-every #:init-value 2
              #:init-keyword #:sing-every))

(define-method (cat-sing (cat <cat>))
  (format #t "~a: ~a\n"
          (slot-ref cat 'name)
          (slot-ref cat 'noise))
  (run-delay (cat-sing cat)
             (slot-ref cat 'sing-every)))

;; An example cat
(define mittens (make <cat> #:name "Mittens"))

(define (run-demo)
  (start-agenda
   (make-agenda #:queue
                (make-q* (wrap (cat-sing mittens))))))
scheme@(guile-user)> (run-demo)
Mittens: Meow
Mittens: Meow

Here we busted out GOOPS, Guile's object-oriented programming system. You don't need to use GOOPS and 8sync together, but they're kind of a fun pair to combine. As you can see, we've created a cat class and allowed it to sing every specified number of seconds. Now you may have noticed that this looks an awful lot like the snoring example! And that's true. But what's fun about this is that we could make a whole group of cats… say a cat alleyway band… and put them all to work singing at the same time.

(define cat-band
  (list
   ;; The main vocalist... pretty noisy
   (make <cat>
     #:name "Fitz"
     #:noise "Meow... meow meow..... meow!"
     #:sing-every 1)
   ;; Sally the soprano
   (make <cat>
     #:name "Sally"
     #:noise "Mi mi mi miiiiiiii!"
     #:sing-every 2)
   ;; Bass player... a bit slower of a rhythm
   (make <cat>
     #:name "Harry"
     #:noise "*Bum ba dum bum ba dum...*"
     #:sing-every 3)
   ;; Petal the percussionist!
   ;; She's just crashing together some garbage cans
   (make <cat>
     #:name "Petal"
     #:noise "*Crash!  Crash!*"
     #:sing-every 4)))

(define (run-demo)
  (start-agenda
   (make-agenda #:queue
                (list->q
                 (map (lambda (cat)
                        (wrap (cat-sing cat)))
                      cat-band)))))
scheme@(guile-user)> (run-demo)
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Harry: *Bum ba dum bum ba dum...*
Petal: *Crash!  Crash!*
Fitz: Meow... meow meow..... meow!
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Harry: *Bum ba dum bum ba dum...*
Fitz: Meow... meow meow..... meow!

Now we've got some nice variation going! Yikes, you'd think that such a band would be liable to wake someone up!

3 Pestering a human

Hey… that's not such a bad idea. Let's bring back our snoring friend as a sleepy human.

(define-class <human> ()
  ;; How irritated the human currently is
  (irritation #:init-value 0
              #:accessor human-irritation)
  ;; How long it takes for the human to wake up
  (awake-threshold #:init-value 50
                   #:getter human-awake-threshold))

(define %human-act-every-secs 6)

(define-method (human-sleep-or-suffer (human <human>))
  "Continue sleeping or maybe go crazy"
  (cond ((< (human-irritation human)
            (human-awake-threshold human))
         (display "==Human==: *Zzzzzzz....*\n"))
        ;; We're awake, so be angry
        (else
         (display "==Human==: AUGH!!!!!  SHUT UP YOU CATS!!!\n")))
  (run-delay (human-sleep-or-suffer human)
             %human-act-every-secs))

(define-method (human-maybe-wake-up (human <human>))
  "Possibly make a waking up noise, if we just hit the limit"
  ;; If we just hit the awake level, snort awake
  (if (eqv? (human-irritation human)
            (human-awake-threshold human))
      (begin
        (display "==Human==: *SNORT* Huh????\n")
        #t)
      #f))

(define-method (human-be-irritated! (human <human>))
  "Get annoyed by the cats"
  ;; Increase the irritation by one
  (set! (human-irritation human)
        (+ (human-irritation human) 1))
  ;; Maybe wake up
  (human-maybe-wake-up human))

As we can see, this human is sleepy and wants to stay asleep, but might not be able to be so lucky…

In order for this whole thing to play out right, we're going to need to modify our cat singing operation in order to actually pass along irritation to a human. Here's the new definition:

(define-method (cat-sing (cat <cat>) (human <human>))
  (format #t "~a: ~a\n"
          (slot-ref cat 'name)
          (slot-ref cat 'noise))
  ;; New: Annoy the human
  (let ((did-we-wake-them
         (8sync-delay (human-be-irritated! human)
                      ;; human reaction time is 1/5 of a second
                      (/ 1 5))))
    (if did-we-wake-them
        (format #t "~a: Hee hee hee!  =^.^=\n"
                (slot-ref cat 'name))))
  (run-delay (cat-sing cat human)  ; New: We also now pass the human in here
             (slot-ref cat 'sing-every)))

The method now takes an additional argument, a human to annoy, which we also pass in again when we loop with our run-delay call.

In the new adjusted model of our universe, the cats irritate the human, but the human has a slight reaction time (a fifth of a second) before it notices the noise the cat has made. (The cats play a bit slower too, as they're carefully watching the human's reactions… they savor the opportunity to irritate the human and wake them up!)

To pull this off, in the new annoying-the-human section, you'll notice something we haven't used before.. "8sync-delay". Here's the thing: even though we want to model the 1/5 of a second human reaction time, we don't want to separate our function into several seprate functions, and we don't want to block the other cats from singing. So 8sync-delay does something pretty cool here: it suspends the "cat-sing" function mid-execution and creates a run-request for 1/5 of a second in the future to call the "human-be-irritated!" function. Once "human-be-irritated!" has finished running, it wakes back up "cat-sing" from where it left off. Not only that! Our cat wants to know whether or not irritating the human woke it up or not, so it needs that information to be passed back from "human-be-irritated!". Luckily for us, 8sync-delay handles this for us. Pretty cool!

(As you may have guessed, we could have run this without a delay with just "8sync" instead of "8sync-delay". There wouldn't have been much reason to do this, since without delays we could have just called the function, but you could imagine some scenarios where there would… maybe a system where you only want to read a few chunks of data from a file at a time, or something else where cooperative behavior is incentivized.)

Okay, that's all good and well, but we should run the function. Our code for booting up the agenda looks a bit more complicated now, but that's because we're building the human first and passing the information about the human to the cats, and so on.

(define (run-demo)
  (let* ((our-human (make <human>))
         (initial-cat-noises
          (map (lambda (cat)
                 (wrap (cat-sing cat our-human)))
               cat-band))
         (initial-human-task (wrap (human-sleep-or-suffer our-human)))
         (initial-tasks (list->q
                         (cons initial-human-task
                               initial-cat-noises))))
    (start-agenda
     (make-agenda #:queue initial-tasks))))
scheme@(guile-user)> (run-demo)
==Human==: *Zzzzzzz....*
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Harry: *Bum ba dum bum ba dum...*
Petal: *Crash!  Crash!*
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Fitz: Meow... meow meow..... meow!
Harry: *Bum ba dum bum ba dum...*
Fitz: Meow... meow meow..... meow!
Petal: *Crash!  Crash!*
Sally: Mi mi mi miiiiiiii!
Fitz: Meow... meow meow..... meow!
==Human==: *Zzzzzzz....*
Fitz: Meow... meow meow..... meow!
Harry: *Bum ba dum bum ba dum...*
Sally: Mi mi mi miiiiiiii!
    [... After a while ...]
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Petal: *Crash!  Crash!*
Fitz: Meow... meow meow..... meow!
Harry: *Bum ba dum bum ba dum...*
==Human==: *SNORT* Huh????
Harry: Hee hee hee!  =^.^=
Sally: Mi mi mi miiiiiiii!
Fitz: Meow... meow meow..... meow!
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Harry: *Bum ba dum bum ba dum...*
Fitz: Meow... meow meow..... meow!
Petal: *Crash!  Crash!*
==Human==: AUGH!!!!!  SHUT UP YOU CATS!!!
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!

4 The grand finale

So this is all good and well, but one thing you might notice: once the human wakes up, the cats (and the human) just go on making noise forever! It would be more interesting if things came to a finish somehow. There's an obvious way for that to happen: the human totally loses it after a certain period of time and jumps out the window! (But don't worry, since this is a cartoonish universe, the human just lands in a garbage dumpster and isn't physically hurt.)

So let's add a new method to see whether or not the human is actually even sane anymore, and a new slot on the human class to track whether or not the human has really lost it (jumped out the window)! We'll also modify the human-sleep-or-suffer method to make use of that check for dramatic effect!

(define-class <human> ()
  ;; How irritated the human currently is
  (irritation #:init-value 0
              #:accessor human-irritation)
  ;; How long it takes for the human to wake up
  (awake-threshold #:init-value 50
                   #:getter human-awake-threshold)
  ;; If this is set to #t, we've completely lost it
  (lost-it #:init-value #f
           #:accessor human-lost-it))

(define %human-maximum-sanity 100)
(define-method (human-is-still-sane? (human <human>))
  (< (human-irritation human) %human-maximum-sanity))

(define-method (human-sleep-or-suffer (human <human>))
  "Continue sleeping or maybe go crazy"
  (cond ((< (human-irritation human)
            (human-awake-threshold human))
         (display "==Human==: *Zzzzzzz....*\n"))
        ;; Too far... go nuts!
        ((not (human-is-still-sane? human))
         (display "==Human==: That's it... I CAN'T TAKE IT ANYMORE!!!\n")
         (display "**** The human jumps out the window! ***\n")
         (display "**** The human lands, dramatically, in a dumpster! ***\n")
         (display "**** The dumpster is full of garbage! ***\n")
         ;; We've lost it, game over!
         (set! (human-lost-it human) #t))
        ;; We're awake, so be angry
        (else
         (display "==Human==: AUGH!!!!!  SHUT UP YOU CATS!!!\n")))
  (run-delay (human-sleep-or-suffer human)
             %human-act-every-secs))

Now, if the human has really jumped in the dumpster, the human shouldn't be yelling anymore. Furthermore, that's a clear indicator to the band of alley cats that they put on a good show. So! What we really ought to do is end the show right then and there (stop the agenda) and let the cats celebrate.

(define (grand-finale cats)
  (let* ((cat-names (map (lambda (cat)
                           (slot-ref cat 'name))
                         cats))
         (cats-combined (string-join cat-names ", ")))
    (format #t "~a: \"Thanks for coming to our show!\"\n"
            cats-combined)
    (format #t "* The cats take a bow and exit alley left! *\n")))

(define (run-demo)
  (let* ((our-human (make <human>))
         (initial-cat-noises
          (map (lambda (cat)
                 (wrap (cat-sing cat our-human)))
               cat-band))
         (initial-human-task (wrap (human-sleep-or-suffer our-human)))
         (initial-tasks (list->q
                         (cons initial-human-task
                               initial-cat-noises))))
    (start-agenda
     (make-agenda #:queue initial-tasks)
     #:stop-condition
     (lambda (agenda)
       ;; If the human has gone crazy, it's game over
       (human-lost-it our-human)))

    ;; Made it this far?  The cats put on a good show!
    ;; Let them take a bow!
    (grand-finale cat-band)))

Now let's run this thing:

scheme@(guile-user)> (run-demo)
==Human==: *Zzzzzzz....*
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Harry: *Bum ba dum bum ba dum...*
Petal: *Crash!  Crash!*
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Fitz: Meow... meow meow..... meow!
Harry: *Bum ba dum bum ba dum...*
Fitz: Meow... meow meow..... meow!
Petal: *Crash!  Crash!*
Sally: Mi mi mi miiiiiiii!
Fitz: Meow... meow meow..... meow!
==Human==: *Zzzzzzz....*
Fitz: Meow... meow meow..... meow!
Harry: *Bum ba dum bum ba dum...*
    [... After a while ...]
Petal: *Crash!  Crash!*
Fitz: Meow... meow meow..... meow!
Harry: *Bum ba dum bum ba dum...*
==Human==: *SNORT* Huh????
Harry: Hee hee hee!  =^.^=
Sally: Mi mi mi miiiiiiii!
Fitz: Meow... meow meow..... meow!
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Harry: *Bum ba dum bum ba dum...*
Fitz: Meow... meow meow..... meow!
Petal: *Crash!  Crash!*
==Human==: AUGH!!!!!  SHUT UP YOU CATS!!!
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
    [... A bit longer still ...]
Harry: *Bum ba dum bum ba dum...*
Fitz: Meow... meow meow..... meow!
Sally: Mi mi mi miiiiiiii!
Fitz: Meow... meow meow..... meow!
==Human==: That's it... I CAN'T TAKE IT ANYMORE!!!
**** The human jumps out the window! ***
**** The human lands, dramatically, in a dumpster! ***
**** The dumpster is full of garbage! ***
Fitz, Sally, Harry, Petal: "Thanks for coming to our show!"
* The cats take a bow and exit alley left! *

Horray! How's that for a production?

As you can see, we provided a method to the #:stop-condition flag of start-agenda. By default this is set to "stop-on-nothing-to-do", which often works well but is a bit heavy (it checks manually for whether anything is scheduled or any ports are still being listened to in the agenda on every agenda tick) and as you can see in applications which loop infinitely, may not be what you want. Luckily, you can change it easily!

That's the end of this tutorial… you've learned how to work with the 8sync agenda generally. However, we've only barely taken advantage of what 8sync is useful for (our example focused on delays, which are nice but are not really the most intersting thing). In the next section we'll learn how to do nonblocking IO using 8sync's port support.

Thanks to Asheesh Laroia, Arne Babenhauserheide, and Caden Howell for giving feedback on this tutorial!

Author: Christopher Allan Webber

Created: 2016-04-27 Wed 10:36

Emacs 24.5.1 (Org mode 8.2.10)

Validate