Tuesday, May 29, 2007

Cicada Example: Abstracting Server Details

As promised, I'd like to present an example or two of the new Cicada framework I just posted about.

Before tackling these examples, you'll want to read the paper on Termite, as all the background needed to understand them can be found there.

An Opposite Server

Here's the code for an opposite server. You pass it a positive number, it'll give you back a negative one. Pass it a list, it will reverse it. Pass it true, it'll hand back false. You get the idea. Notice the use of pattern matching and guards to implement this:

(define opposite-server1
  (spawn (lambda ()
           (let loop ()
             (cicada/recv
              [(,from ,tag ,x) (number? x) (! from (list tag (* -1 x)))]
              [(,from ,tag ,x) (string? x) (! from (list tag (string-reverse x)))]
              [(,from ,tag ,x) (boolean? x) (! from (list tag (not x)))]
              [(,from ,tag ,x) (list? x) (! from (list tag (reverse x)))]
              [(,from ,tag ,x) () (! from (list tag x))])
             (loop)))))

To make calling this server easy, we can take a hint from the Termite paper and Erlang and define !? as follows:

(define (!? pid mesg)
  (let ((t (make-tag)))
    (! pid (list (self) t mesg))
    (cicada/recv [(,tag ,reply) (eq? t tag) reply]
                 (after 200 (error (format "Failed to receive reply about message ~a" mesg))))))

In this case, I've decided that a latency of 200 milliseconds should result in an error. Naturally, this behavior could be customized to be any number of alternatives.

Calling our new server now reduces to:

(assert equal? -7 (!? opposite-server1 7))
(assert equal? #f (!? opposite-server1 #t))
(assert equal? "oof" (!? opposite-server1 "foo"))
(assert equal? '(x 2 1) (!? opposite-server1 '(1 2 x)))
(assert equal? + (!? opposite-server1 +)) ; unknown type. just return it.  

Adding Abstraction

One detail I noticed when writing the above server was how the it needed to return back a message in just the right format for !? to function. Why should it be the responsibility of the server author to make sure this low level messaging format detail was done right?

We can actually do better. We can abstract out those server details, and let the server author just focus on code. For starters, we can define a procedure to create this specific class of server (named tagged response for the fact that all responses include a, well, tag).

(define (spawn-tagged-responder handler)
  (spawn (lambda ()
           (let loop ()
             (cicada/recv
              [(,from ,tag . ,mesg) () (handler mesg
                                                 (lambda (result)
                                                   (! from (list tag result))))])
             (loop)))))

Next we can re-write our opposite server, but this time without worrying about the details of formatting a response. The server can just focus on what it needs to do.

(define opposite-server2
  (spawn-tagged-responder (lambda (mesg !!)
                            (match mesg
                              [(,x) (guard (number? x))  (!! (* -1 x))]
                              [(,x) (guard (string? x))  (!! (string-reverse x))]
                              [(,x) (guard (boolean? x)) (!! (not x))]
                              [(,x) (guard (list? x))    (!! (reverse x))]
                              [(,x)  (!! x)]))))

Finally, we can show this all works by invoking more or less the same code above:

(assert equal? -7 (!? opposite-server2 7))
(assert equal? #f (!? opposite-server2 #t))
(assert equal? "oof" (!? opposite-server2 "foo"))
(assert equal? '(x 2 1) (!? opposite-server2 '(1 2 x)))
(assert equal? + (!? opposite-server2 +)) ; unknown type. just return it.     

In just 9 lines of code we've simplified the implementation of our server in a significant way. Gosh I love the bendy nature of Scheme.

No comments:

Post a Comment