[Fluxus] Continuations, take 1

David Kaloper dkaloper at mjesec.ffzg.hr
Thu Feb 17 09:27:55 PST 2011


On Thu, Feb 17, 2011 at 09:33, Kassen <signal.automatique at gmail.com> wrote:
> Hi David!
> This looks exciting.
>
>> The story is this: after recently discovering this wonderful piece of
>> software
>> and playing around for a bit, I got tired of the inversion of control that
>> every-frame imposes.
>
> I have to admit I'm already confused a bit at this point. What do you mean
> by a "inversion of control"? I get that you want to get out of the simple
> "loop" structure and go towards something more "event-like".

It is more than that.

Consider the following, vague, program-oid:

"Begin with zero. Wait for three seconds, then throw a dice. Add the number to
the number you have, then wait for two more seconds. Then yell something that
number of times and restart with the first wait, remembering the number."

It's pretty straightforward to write:

(let lawp ([a 0])
  (sleep 3)
  (let ([b (+ a (random 6) 1))
    (sleep 2)
    (yell b)
    (lawp b)))

But of course, using sleep would completely block Fluxus. You can't afford your
logically continuous sequence of steps to correspond to a continuous sequence of
actually executed instructions, simply because there are other things to do.

You have to tie in with other steps that happen (sort-of) simultaneously.
Threading aside, what you would do is something like this:

(define st (list 'here 0 (time-now)))

(every-frame
  (let ([state (car st)] [a (cadr st)]
        [last (caddr st)] [now (time-now)])
    (cond
      [(and (eq? st 'here) (> (+ last 3) now))
       (set! st (list 'there (+ a (random 6) 1) now))]
      [(and (eq? st 'there) (> (+ last 2) now))
       (yell a)
       (set! st (list 'here a now))])))

Ugh. That's my inversion of control. (The `match' macro would help, but the
basic outline remains.) What used to be a sequence of steps is now an ugly
dispatching function that has to terminate as fast as it can, each time
reconstructing what it had in mind last time around, then saving what it knows
now. It's a state machine. You probably wrote zillions of those.

It's a problem with waiting for stuff to happen, I think. To multiplex separate
logically continuous, but not temporally coherent streams of steps, you
typically have to ride a monocycle juggling with one, and fending off an army of
depraved monkeys with a knife in the other hand. Threads are a pre-canned
mechanism, but it would get awfully complicated making sure some sequences of
steps in one thread happen without intervening interruption by another. Plus the
question of who controls GL state. That, and racket threads don't work in
Fluxus. :)

But Fluxus already comes with a simple event-dispatching mechanism. You can use
that to recur indirectly:

(define-syntax-rule
  (after n b b1 ...)
  (spawn-timed-task (+ (time-now) n) (lambda () b b1 ...)))

(begin
  (define (here a)
    (after 3 (let ([b (+ (random 6) a 1)]) (there b))))
  (define (there a)
    (after 2 (yell a) (here a)))
  (here 0))

Except it won't work. Tasks ran with spawn-timed-task can't spawn-timed-task
other tasks. This is what got me going. But even with that fixed, it's still not
neat. There is clutter, and you have to tail-call (umm tail-schedule?) the next
thing because you still have to terminate the current task before anything else
can happen. (That's about as far as, for example, Scala reactive actors take
it.)

So this is not about events themselves. Basic events are there. This is about
writing code on top of that, code that looks and feels unbroken and continuous
if it goes about doing an unbroken and continuous thing and maybe waits for
stuff in between.

Now you can just use the first snippet, exchanging `sleep' with `restart-after'.
And you can do it deeply nested, too. From within a control structure, for
example:

(for* ([a (list "marry had" "jimmy asked for")]
       [b (list "a batmobile" "A BATMOBILE!")])
  (displayln (string-append a " " b))
  (restart-after 1))

And to add to the general feeling of unbroken-ness, you get your GL state or
grab back the way it was when you left. So it doesn't look like leaving at all.

I'm working on a script that uses this. First experience - explicitly maintained
structures tend to disappear and everything is simpler. For tight stuff
happening frame-to-frame it's faster to use `spawn-task' directly, but for
big-picture things, this is... neat.

Hope it's clearer now.

Cheers.

-- 
"Linear Time is wrong and suicidal." -- Gene Ray



More information about the Fluxus mailing list