A couple weeks ago, I tooted:
CoffeeScript ought to take this opportunity to add new features that help with async e.g. tail calls, continuations.
Now there’s a problem with this: it’s a core part of CoffeeScript’s usability and resultant popularity that it compiles to readable JavaScript. If you want to introduce new execution semantics, a straightforward one-to-one syntax transformation is not such a viable option.
But I maintain that, given we’re reinventing the language we work with on the
web, now’s as good a time as any to reexamine the tools we use to write
programs. We’re all annoyed by working with callbacks, and with good reason.
Programming with explicit callbacks introduces race conditions and uncertainty;
it’s not the syntactic burden of typing function
that causes the real pain,
it’s the creation of regions of your program whose order of execution is
unpredictable.
In the Ruby world, we’ve been using fibers to great effect to hide the asynchronous nature of code. A fiber is just a special kind of single-use function, whose execution can be suspended and resumed by the user. They are perfect for pausing a block of code while some I/O is done, but they do not block the interpreter: when you pause a fiber the interpreter can carry on doing other work until the fiber is resumed. Ilya’s first example is perfect:
def http_get(url)
f = Fiber.current
http = EventMachine::HttpRequest.new(url).get
# resume fiber once http call is done
http.callback { f.resume(http) }
http.errback { f.resume(http) }
return Fiber.yield
end
Calling http_get()
initiates an async HTTP request, and just before returning
it ‘yields’, i.e. it pauses the current fiber. When the request completes, we
resume the fiber with the response. A call to http_get()
looks like
synchronous code, but behind the scenes the I/O is being done in a way that
doesn’t block the interpreter.
I thought it would be fun to try this out in JavaScript, so I hacked up a very
basic Scheme-like interpreter to support fibers, and called it Fargo.
Scheme usually includes a function called call-with-current-continuation
or
call/cc
, of which fibers are a simplified version. Call/cc
allows the
current state of a computation (the “continuation”) to be saved and resumed any
number of times. This means, at least in a naive implementation, that the saved
state of the stack must be copied every time you invoke a continuation,
otherwise state would leak between uses. Fibers can only be resumed once from
their last yield-point, making running and resuming them cheaper.
Fibers as implemented in Ruby also require the user to explicitly initiate a
fiber. Supporting call/cc
requires constantly keeping track of the current
continuation since it may be captured at any time. Because fibers must be
explicitly initiated, when not in a fiber we can use a faster stackless
execution engine which doesn’t need to track state so much.
Here’s an example in Fargo of getting a value that’s produced by an async API
(set-timeout
) without using callbacks in the calling code:
; get-message function: captures the current fiber, yields,
; then resumes after a timeout when we have a message to return
(define (get-message)
(define f (current-fiber))
(set-timeout (lambda ()
(f "Hello, world!")
) 5000)
(yield))
; main program fiber
(define main (fiber ()
; get the message using a sync-looking API
(define msg (get-message))
; alert the message
(alert msg)
))
; run the main fiber
(main)
Because it’s tail-recursive, you can use recursion to implement infinite streams:
> (define stream (fiber ()
(let loop ((i 0))
(yield i)
(loop (+ i 1)))))
> (stream)
; => 0
> (stream)
; => 1
> (stream)
; => 2
So where is Fargo going? Right now, with my current ban on committing myself to any new large side-projects, it remains a quick (12 days old) experiment. Maybe someone will like the idea enough to turn it into a production language but for now I’m happy to let people try it out and let me know what they think. I’ve been making small improvements to it the last few days to get it to run as much of Heist’s core library as possible and it now runs all the macros and the list and numeric libraries.
I’m going to keep thinking about how we can make async programming easier and I’d love your ideas – the nice thing about working on a Lisp is that changing the interface it provides is really simple.