Beware of yielding numbers when awoken from a condition

A difference in behavior: if a Routine yeilds a number when it’s called from the interpreter, then you just get that number back once. But if the same number-yileding happens after the Routine was awaken from a Condition (or something that use that, e.g a FlowVar), then the yielded number will put the Routine on a clock. Example:

v = FlowVar()

(x = Routine { loop {
	postln("x got" + v.value); v.value.yield	
}}) // -> hang

v.value = 1 
// posts "x got 1" and repeats it every second


// flowvar is still bound to 1
// x got 1
// -> 1
// But only does that once!
// There's no rescheduling this time. // this will reschedule from the REPL

Hm, a “reactive stream” concept, if it’s using routines both to return data and to yield control-flow tokens / timing values, will have to differentiate between the two – but there’s only one yield. I think that’s the root of this issue. If a Routine ever ends up on a clock, then its yield values are used for timing, not for arbitrary data… but here, yield is also used for data, and if the data are numeric, then the clock has to decide whether the number is a time or a data point.

I’m not sure whether the best solution is to differentiate them somehow, or to write an alternate FlowVar that doesn’t reschedule…?


I agree that’s it’s a bit difficult to make a SC routine properly work when called directly and also from a clock. But the routine can actually tell the difference as long as you pass something other than a number as argument to next when calling it directly

(r = Routine { arg in;
	loop {
		if(in.isNumber) {
			"Probably called from a clock".postln;
			in = 1.yield
		} {
			in = "Surely not called from a clock".postln.yield; }
}); // try as much as you like while it's playing
// Of course, you can fool it if you pass a number in:

I concede after a few more experiments with this that it is a bit of a rabbit hole though, because it’s hard to write larger correct programs that way. A fairly substantial issue is also the lack of library support for this approach.

Various things like CondVar that aim to provide a thread-based view of Routines take it for granted that they’re going to be called exclusively from a routine that is running on a clock, or least one that is implementing the (single) clock protocol, meaning that there will be no calls to next after the routine yielded a symbol like \hang, \wait etc., until after some other agreed method is called, like signal. Because this what clock does, i.e. it never reschedules again something that yielded a non-number. Even though CondVar has beefy help page compared its predecessor, it failed to state this assumption, although I eventually inferred it from the buggy behavior when next is called on the routine, coupled with the complete lack of such an example where next might be called explicitly, among the half-dozen examples in the help of CondVar.

In hindsight, this assumption is fairly reasonable as it simplifies implementation and targets what seem to be the common use pattern. An argument in its favor is that there should be a separation of concerns between “conductor” timing and the production of other data, although something like Events to actually multiplex those, albeit in dictionaries, so that solves the problem of data typing. And EventStreamPlayer does the obvious de-multiplexing business of sending delta data to the clock and the rest to the server as various kinds of OSC commands.

There’s more incompatibility with synchronization lurking in the data-handling layers, because sending something like \hang for dur to the EventStreamPlayer doesn’t exactly work

Pbind(\dur, Pseq([1, \hang, 1])).play
^^ ERROR: Message 'schedBundleArrayOnClock' not understood. RECEIVER: hang

and some hanged endless note, probably because it failed to gate out the synth after throwing that error. One would have to pre-translate those hangs into silences or some default phrase etc.

So, in a sort of summary, one has to limit their use of synchronization primitives like CondVar at the top level routines that run on a clock, at least if one wants to use the existing library bits. And conversely, even though the streams instantiated from Patterns are instantiated as routines too, e.g. with Pattern.asStream, one cannot really hang or wait in those. Basically, there’s a form of typing imposed on routines by the expectations in most parts of the class library.