Temporal recursions share much in common with co-routines, and have in various guises been used since the 1960s. Within the audio and music communities the most well known precedents are Collinge's Moxie[19], Dannenberg's CMU MIDI toolkit[21], and McCart- ney's SuperCollider[57]4. Impromptu[78] and subsequently Extempore, have extended
these ideas incorporating a richer application of temporal recursion; most signicantly the application of continuations to temporally recursive structures[77].
A temporal recursion is most simply dened as any block of code (function, method, etc..) that schedules itself to be called back at some precise future time (where precision is related to latency). A standard recursive function could be considered to be a temporally recursive function that calls itself back immediately, with zero temporal delay.
;; A standard recursive function (define my-func (lambda (i) (println 'i: i) (if (< i 5) (my-func (+ i 1)))))
;; A temporally recursive function with 0 delay
;; (callback (now) my-func (+ i 1)) ~= (my-func (+ i 1)) ;; (now) means immediately
(define my-func (lambda (i)
(println 'i: i)
(if (< i 5)
(callback (now) my-func (+ i 1)))))
Listing 6: Recursions
In Listing 6 (callback (now) my-func (+ i 1)) serves a similar function to (my-func (+ i 1)); both are responsible for calling back into my-func immediately, passing an in- cremented value for i. However, the way in which these two recursive calls operate is substantially dierent. The temporal recursion, that is formed by the recursive call (callback (now) my-func (+ i 1)), is implemented as an event distinct from the cur- rent execution context. While the call (my-func (+ i 1)) maintains the control ow, and potentially (assuming no tail optimisation) the call stack, the (callback (now) my-func (+ i 1)) schedules my-func and then breaks to the top level, at which point control ow is handed back to the real-time scheduler.
One pragmatic distinction between a standard recursion and an immediate (no tem- poral delay) temporal recursion is that the temporal recursion does not keep adding new
stack frames to the call stack. Because of this, a temporal recursion must maintain any argument values that are passed (maintained) by the temporal recursion. The arguments must be maintained by the temporal recursion event.
The distinction between standard and temporal recursions becomes more obvious once the temporal recursion is scheduled beyond (now). Because callback returns control back to the scheduling engine, it is possible to interleave the execution of multiple tem- poral recursions concurrently. Listing 7 interleaves two temporal recursions. First a runs then b then a then b etc..
(define a (lambda (time)
(println "I am running a")
(callback (+ time 40000) a
(+ time 40000)))) ;; argument time
(define b (lambda (time)
(println "I am running b")
(callback (+ time 40000) b
(+ time 40000)))) ;; argument time
(let ((time (now)))
(a time)
(b (+ time 20000)))
Listing 7: Temporal Recursion
In Listing 7 time is passed as a parameter to both function a and function b, with both a and b incrementing time at the same rate. The interleaving is dened by the initial oset of 20000. a and b are substantially the same, and can be abstracted into c.
(define c
(lambda (time name)
(println "I am running: " name)
(callback (+ time 40000) c
(+ time 40000) name))) ;; arguments time and name
(let ((time (now)))
(c time "a")
(c (+ time 20000) "b"))
Listing 8: Encapsulated state
Listing 8 spawns two temporal recursions, both executing over c. An important and not immediately obvious benet of temporal recursion is state encapsulation. Listing 8 starts two concurrent temporal recursions one with a name value of "a" and the other with a name value of "b". name and time are maintained independently by their respective temporal recursions. Of course any number of independent temporal recursions over function c could be started (resources notwithstanding), each with its own independent state for time and name.
As well as independent state it is also possible to introduce shared state for temporal recursions. In Extempore closures provide the vehicle for introducing shared, but still encapsulated, state for temporal recursions. Consider a variation to function c that introduces a captured variable count.
(define c
(let ((count 0)) (lambda (time name)
(println "I am running: " name " count: " count) (set! count (+ count 1))
(callback (+ time 40000) c
(+ time 40000) name)))) ;; time and name
(let ((time (now)))
(c time "a")
(c (+ time 20000) "b"))
Listing 9: Encapsulated shared state
In Listing 9 count is shared between both temporal recursions but is still encapsu- lated. Most importantly, because temporal recursions are non pre-emptive, access to the shared variable count is strictly ordered - temporally ordered. This makes shared temporal recursion state easy to reason about. Strict temporal ordering removes all but one ambiguity - the case of two temporal recursions having exactly the same scheduled time. In Extempore's case scheduled callbacks with identical scheduled times are called in the order in which they were added to the scheduler - FIFO.
There is no requirement for the callback time to be periodic. By adjusting the increment to time a temporal recursion can be aperiodic or sporadic, as demonstrated in Listing 10.
;; an example of aperiodic temporal recursion ;; random duration of 1000, 10000 or 100000 ticks
(define c
(lambda (time name duration) (println "I am running: " name)
(callback (+ time duration) c
(+ time duration) name ;; time and name
(random '(1000 10000 100000))))) ;; duration
(c (now) "a" 1000)
Listing 10: Aperiodic schedules
Extempore's temporal recursions support, not only the start time of a temporal re- cursion, but also the deadline of a temporal recursion. An execution deadline constraint can be added to any callback to provide an exception handling pathway for code that executes beyond its scheduled deadline. Listing 11 shows an example of a single temporal recursion that uses more than its allocated time.
(define d (lambda (time)
(println "time lag: " (- (now) time))
;; waste some time
(dotimes (i 400000) (* 1 2 3 4 5))
(callback (+ time 1000) d
(+ time 1000))))
(d (now))
Listing 11: Time lag!
more time than has been allocated (1000 ticks). One answer to this problem would be to modulate the callback rate to better manage the execution time.
(define d (lambda (time)
(println "time lag: " (- (now) time))
;; waste some time
(dotimes (i 5000) (* 1 2 3 4 5))
;; self regulate optimial callback time
;; by passing new revised time rather than static time.
(callback (+ time 1000) d
(+ time 1000 (- (now) time)))))
(d (now))
Listing 12: Time regulation
In Listing 12 the temporal recursion will self-regulate. If concurrency was the only consideration then this might be an acceptable option. However, temporal recursions are not only about providing concurrency but also temporal constraint. It is not enough to run many things, it is necessary to run many things at precisely scheduled times. So the above self regulation option is not an option for most real-time domains.
Instead of modulating the timing accuracy of a temporal recursion to support the performance prole of the hardware, a preferable solution would be to inform the pro- grammer that the precise timing was not supported by the hardware prole - at least not with the current performance prole of the code.
Extempore supports this idea by providing an explicit timing deadline constraint for each temporal recursion. An optional argument to callback provides a maximum execu- tion time constraint after which an exception will be thrown to alert the programmer to
the temporal recursions inability to meet its execution deadlines. (define d
(lambda (time)
(println "time lag: " (- (now) time))
;; waste some time
(dotimes (i 500) (* 1 2 3 4 5))
;; added execution contraint deadline of 900
(callback (cons (+ time 1000) 900) d
(+ time 1000))))
(d (now))
Listing 13: Time constraints
Adding an execution constraint of 900 in Listing 13 results in an exception hook being called if function d does not complete its execution in under 900 ticks. This style of temporal constraint ensures that code either (a) meets its deadlines or (b) fails gracefully. Ideally the system would provide these temporal constraints (i.e. the 900 ticks) with- out making the programmer calculate the complex timing interrelationships between all possible temporal-recursions. In practice this is a non-trivial problem which is made all the more dicult in live programming scenarios where the overall behaviour of the system is completely run-time modiable. This run-time modiability makes static tem- poral analysis very challenging indeed, although is an area where Extempore can, and should improve!
Extempore's temporal recursion events may be Scheme closures, Scheme continua- tions, Scheme macros, XTLang closures, XTLang macros, or native C functions, making temporal recursions extremely exible.
chronously. A synchronous sleep requires just a few lines of code. sys:sleep, uses callback for scheduling, as outlined in Listing 14.
(define *sys:toplevel-continuation* '())
(call/cc (lambda (k) (set! *sys:toplevel-continuation* k)))
(define sys:sleep (lambda (duration)
(call/cc (lambda (cont)
(callback (+ (now) duration) cont #t)
(*sys:toplevel-continuation* 0)
#t))))
Listing 14: sys:sleep - full implementation
Listing 14 uses a synchronous style, based on iteration and sys:sleep, rather than a recursion and a callback.
(define a (lambda ()
(dotimes (i 5)
(println "I am running a" i)
(sys:sleep 40000))))
(define b (lambda ()
(dotimes (i 5)
(println "I am running b" i)
(sys:sleep 40000))))
(let ((t (now)))
(callback t a)
(callback (+ t 20000) b))
Listing 15: Synchronous iterators?
Temporal recursion is based on the simple principle that timed events can be scheduled recursively. However, a suitably powerful implementation of temporal recursion (one supporting closures and continuations for example) supports a rich temporal design space enabling an extended set of practical time and concurrency tools.