Shouldn't Events be (directly) composable?

Currently if you do:

// need a non-default synthdef for some obscure reasons (I'll get to that)
(SynthDef(\mysine2, {  |out=0, amp=1, gate=1, freq=555, pan=0| 
	var sing = EnvGen.ar(Env.asr(), gate, doneAction: 2) * SinOsc.ar(freq);
	OffsetOut.ar(out, Pan2.ar(sing, pan, amp));
}).add;)

((instrument: \mysine2, freq: Pkey(\freq) * 2) <> (freq: 300)).play; // silent

it does nothing of course (because <> is not actually defined for Event and more annoyingly since Event has know turned on () <> () is actually nil , whereas prepending an empty Pbind makes it work obviously

(Pbind() <> (instrument: \mysine2, freq: Pkey(\freq) * 2) <> (freq: 300)).play // sound

(because ‘<>’ is defined for patterns “on the left”, while you can have events on the right of <>. Pdef in fact relies on this to <> its envir.

More annoyingly, replacing the instrument with the default one, i.e.

(Pbind() <> (instrument: \default, freq: Pkey(\freq) * 2) <> (freq: 300)).play // silent

also does nothing even with a Pbind, and I don’t know why. Also

(Pbind(\instrument, \default) <> (freq: Pkey(\freq) * 2) <> (freq: 300)).play // still silent
(Pbind(\instrument, \default, \freq, Pkey(\freq) * 2) <> (freq: 300)).play // sound

So, questions:

  • shouldn’t Events be directly composable with <>? I don’t see a lot of difference between an Event and a Pbind. Event could have asPbind as method for example and thus define a <> for itself too. In the absence of this, if you accidentally chain two events (which you easily can with parentheses in a wrong place), you get no sound.

  • Why doesn’t the \default instrument work in the chained Pbind example, unlike my custom instrument?

Pkey(\freq) is not valid in an event. Only Pbind or a variant will evaluate it.

Events should contain fully resolved values. They should not contain patterns. You get no sound here because of sending an invalid frequency, I’m pretty sure (without testing, maybe I’m wrong).

It’s the same as the difference between [Pwhite(0.0, 0.5, inf), [Pwhite(0.5, 1.0, inf)] and Ptuple([Pwhite(0.0, 0.5, inf), [Pwhite(0.5, 1.0, inf)]). You could say “I don’t see a lot of difference between an array of patterns and a Ptuple” but at the end of the day, it’s still the case that collection objects don’t evaluate patterns contained within – only pattern streams do.

Perhaps a way to say it is that patterns are active while collections are passive, with regard to patterns inside them.

It would be reasonable to define event1 <> event2 to return event2.copy.putAll(event1).

Pbind’s pairs are ordered while Event’s are not. I’m guessing it would be fragile to convert an unordered set of pairs into an ordered one – would work under some restrictions, but it’s only a matter of time before someone writes (dur: Pwhite(0.1, 0.5, inf), freq: Pkey(\dur).linexp(0.1, 0.5, 100, 1000)).asPbind and gets annoyed that it doesn’t work. So I would vote no on that.

hjh

1 Like

Actually, you’re quite right. I was fooled by my default frequency in my Synth being pretty close to the one I was thinking I was setting. With a synth change like

(SynthDef(\mysine2, {  |out=0, amp=1, gate=1, freq=200, pan=0| // ...

it becomes obvious the freq is not actually getting set in the chain…

Pbind’s pairs are ordered while Event’s are not.

Indeed, I overlooked that… since I’ve dabbling with making my own Event subclass that does remember the user-set order on keys. Since Pdef actually chains its envir, you can use it a bit more unorthodox way as

Pdef(\testE, Pbind())
Pdef(\testE).envir = Pbind(\freq, 200)
Pdef(\testE).play // ok, freq is set
Pdef(\testE).stop
Pdef(\testE, Pbind(\freq, Pkey(\freq)*2))
Pdef(\testE).play // multiplies the envir one
PdefGui(Pdef(\testE), 10) // not showing envir, of course

And it works to play, but of course this has the downside that you can’t set into it anymore, nor can you use PdefGui properly on it anymore (envir won’t show).

On the other hand, <> could still make some sense for Events (or even Environments), perhaps as an equivalent for .copy.putAll, but I haven’t given it a lot of thought.

Actually a simple “record override” like copy.putAll is not a terribly useful notion of Event composition because there’s no way to do any lookups in the “prior” environment, i.e. no way to get a Pkey equivalent.

So instead one can use proto chaining, and a “recursive resolve”

+ Event {

	/* "record override" not very useful
	<> { arg anEvent;
		^anEvent.copy.putAll(this)
	}*/
	<> { arg anEvent;
		this.proto_(anEvent); ^this;
	}

	atRec { arg key;
		var val = this.at(key), env = this;
		while ({val.isKindOf(AbstractFunction)},
               {val = env.proto.use { val.value }; env = env.proto; })
		^val
	}
}

At least for some basic recursive lookup this seems to work, i.e.

e = (freq: {~freq * 2}) <> (freq: 200, bar: 24);
d = (freq: {~freq + ~bar}) <> e;

e.atRec(\freq) // 400
d.atRec(\freq) // 424

I’m guessing there is a way to break this since the ASTs that BinaryOpFunction builds aren’t as comprehensive as Patterns.

Also, there’s no notion of sequencing here, just composition, and one needs to decide when to resolve the values, i.e. before playing:

+ Event {
	resolve {
		var ev = this.copy;
		this.keysDo { |k| ev[k] = this.atRec(k) };
		^ev
	}
}

e.resolve // ( 'freq': 400 )
d.resolve // ( 'freq': 424 )

I found out where this approach breaks, namely

~freq = 11
e = (freq: r { loop { yield (~freq * 2) } }) <> (freq: 200, bar: 24)
e.resolve // ( 'freq': 22 )

Routines are AbstractionFunction too, but they don’t look up in the use environment for some reason, i.e.

~foo = 12
f = {~foo + 2}
(foo: 44).use { f.value } // -> 46
// but
r = r { loop { yield (~foo + 2) } }
(foo: 44).use { r.value } // -> 14

I’m not seeing a way to fix this right now. Adding extra nesting like yield ({~freq} * 2) has the problem of decoupling the proto advance/recursion from the value recursion. In Patterns you don’t really have this problem because the association is not externally held, i.e. each pattern holds enumerable refs to those patterns it depends on for data.

Also, generally

f = {42}
g = f + 3
g.value // -> 45; In contrast
h = r { loop { (f + 3).yield } } // needs a double "resolve"
h.value // -> a BinaryOpFunction
h.value.value // 45

Now I get to appreciate why embedInStream “recurses” as a function but “returns values” with yield so it can pop that value out of an arbitrary number of nestlings that are not known in advance… And yeah, there’s a way to fix this, but it’s not quite transparent to the routine (writer), i.e.

f = {42}
h = r { loop { (f + 3).value.yield } } 
h.value // 45

The problem with applying this to the Even chaining here is that Routine needs to do the environment change, and it can’t access it in my setup.

This idea – that events can contain arbitrary future calculations, and compose them arbitrarily – is interesting, but I would suggest to implement that in something other than Event.

I think it’s necessary to have an Event that is simply data storage with the capacity to play. That’s in keeping with Collections in general. Collections in SC are generally for already-resolved data.

So your very first example on this topic –

((instrument: \mysine2, freq: Pkey(\freq) * 2) <> (freq: 300)).play;

– is already outside of what Events are designed to do. It wasn’t correct in the first place to expect Pkey(\freq) to resolve.

So then “Actually a simple ‘record override’ like copy.putAll is not a terribly useful notion of Event composition because there’s no way to do any lookups in the ‘prior’ environment, i.e. no way to get a Pkey equivalent” also becomes moot, because Events don’t do Pkey.

It may be valuable to have some object like an Event that does do Pkey – and if there are other changes in the class library that are needed to make this other object interchangeable with Event, I’d be open to that.

But I’m highly skeptical of changing the basic nature of Event. It is now a container for the results of calculations. You’re proposing to change it into a container for future calculations. That is a massive change to the fundamental assumptions behind Events (or even behind collections in general – then, why not a function-composable array?), with a non-trivial risk of breaking existing user code. And object-oriented modeling already has a solution for that: don’t change the base class, add something else that uses the base class.

hjh

I have’t had much time play with this today, but as a quick note, after the “obvious hack” on Routine

+ Routine {
	envir {	^environment }
	envir_ { arg env; environment = env; ^this }

	valueInEnvir { |env ... args|
		var oldenv = this.envir, val;
		this.envir = env;
		val = this.value(args);
		this.envir = oldenv;
		^val
	}
}

I can now do in + Event

	atRec { arg key;
		var val = this.at(key), env = this;
		while ({val.isKindOf(AbstractFunction)}, {
			if(val.isKindOf(Routine),
				{ val = val.valueInEnvir(env.proto) },
				{ val = env.proto.use { val.value } });
			env = env.proto; })
		^val
	}

And on a quick check this works now as expected:

e = (freq: r { yield (~freq * 2) }) <> (freq: 200, bar: 24)
e.resolve // -> ( 'freq': 400 )

So (horrors), I could even turn Events into Patterns basically as they can “pull” from streams now.

I can obvious extend valueInEnvir to the Function-based stuff simply so the interface is prettier (basically it’s equivalnet to f.inEnvir(e).value but it turns out I need to touch more than Function because of the BinaryOp business).

On the other hand, I’ve been indeed thinking along the lines you’ve mentioned that a “mini-container” might be better than having atRec directly resolve (i.e. apply) functions/routines, simply because of compatibility issues with stuff Pmono etc, which stick callbacks (i.e. functions) into Events, and I probably don’t want to be force-evaluating those (on resolve)… Cleanups are a similar issue, but those probably should be done differently anyway. I’m still evaluating what the best fixup for that is. It’s actually one of the reasons I’ve been toying with “enriched Events” like in this experiment.

Strongly suggested to use protect here. If the operation throws an error, then a protect error handler will restore the original environment anyway. Without it, an error would leave the routine in a corrupted state. I’d demonstrated this in the other thread.

I was trying to express that, no matter how cool it is to have streams and functions resolve automagically in an Event (or Event-like structure) – and this is a brilliant idea actually – we still need to have a concept of Event as a container for results of calculations done outside of the Event. That is, your concept need not (even should not) supersede what Event already is.

A similar thing comes up with SynthDef sometimes – a user wonders why it doesn’t handle graph topologies that adapt to argument values, or x or y or z. My position on that is that we need a concept of SynthDef matching the server’s restrictions. Having a “physical” SynthDef doesn’t preclude other structures that use SynthDefs to do things that SynthDef by itself can’t do. But often, people see SynthDef everywhere in the documentation and assume that SynthDef is supposed to be responsible for everything. I don’t think do. SynthDef fulfills a necessary role, as it is. Event fulfills a necessary role, as it is.

I suppose we have to agree to disagree about that, but I think I’m on pretty solid ground. Part of the point of Design Patterns is to reduce the risk of breakage by choosing development strategies other than fundamentally altering base classes for every new requirement. I can’t help but note that the thrust of this thread has been “how to make Event do what I want” rather than “how to create a new superstructure that does what I want.” The former is invasive and risky while the other is not, so I think the latter should be preferred as a default position.

hjh

Let’s not overstate how much surgery I’ve done here on Events. I’ve just reinterpreted its data… on demand. Those methods I’ve added can be entirely in user functions.

If you wonder how this is related to (fixing) cleanups: imagine that if instead of cleanup functions we’d have cleanup routines, implemented with the obvious logic “do cleanup once, then yield nil”. It would not matter how many times that routine-based cleanup gets called, it is intrinsically idempotent.

There’s a way to obtain the exact same behavior with functions, of course, but it needs a bit more packing, i.e. the actual cleanup function has to be the return value of another function (closure) like

{ |userfun| var done = false; { if(done.not) { done = true; userfun.value } } }

The other thing that me ponder more explicit event composition is that–conceptually–one can think of events that carry the cleanup-function reference (which they do presently anyway in the addToCleanup field) as events obtained by composing with cleanup generating event. So one approach could be consider “sticking idempotent cleanups” just in events, and not have pattern streams keep any local copies of such cleanup structures. It’s probably obvious, but I’ll say it anyway: with this approach a cleanup generator/source (e.g. Pfset) would always send its cleanup by “composing it” into the Event, not just on the first event it generates (as it happens right now). Conceptually, all events (not just the first one) generated by a Pfset (with non-nil cleanup) carry a cleanup promise. So, yeah, I am considering making this conceptual idea the actual implementation of cleanups. And further downstream (again in the dataflow perspective), you compose whatever cleanups you receive from upstream (i.e. typically from subpatterns’ streams) with your own cleanups, if you generate any. (I said “typically” because e.g. a Pchain’s event dataflow graph differs from its sub-pattern graph. A Pchain looks like root of a “flat” tree of from the pattern perspective (i.e. abstract syntax tree perspective), but it actually “pipes data” between its children, so from a dataflow perspective, a Pchain creates a linear chain of streams among its children, with the Pchain itself at the most downstream point. )

I’ve just reinterpreted its data… on demand. Those methods I’ve added can be entirely in user functions.

Ok.

I guess I should explain a bit why I’m being cautious about it. Scztt has mentioned “technical debt.” One of the ways SC has incurred technical debt is by a developer saying “Wouldn’t it be cool if…?” and then someone had committed an implementation, without much review or discussion, which worked most of the time but failed in some cases (e.g. Pfset vs Pchain, or my own failed design, the previous version of Rest, which you didn’t see). Then someone comes into the project and starts noticing, and becoming annoyed with, gaps or inconsistencies.

The cause of those gaps and inconsistencies was originally moving too quickly… and, I learned the hard way in the past that a clever idea to sidestep a problem needs extra scrutiny. I’m trying now to play that role of scrutinizing – bear with me.

If you wonder how this is related to (fixing) cleanups…

I hadn’t. This appears/appeared to be a separate topic… not anymore, but initially.

Now that I might understand a bit more…

So one approach could be to consider “sticking idempotent cleanups” just in events, and not have pattern streams keep any local copies of such cleanup structures. … with this approach a cleanup generator/source (e.g. Pfset) would always [put] its cleanup into every Event, not just on the first event it generates

That’s a good idea (with a couple of tweaked words) – very good idea.

I do have a question, though: Why do you feel that event composition is the best way to do this?

Event composition, in a general sense, is something that a user could do for any purpose. Cleanup is a specific purpose. A/ If events are composed as a calculation strategy, then the composed-in calculations have to be resolved before the event takes action (i.e. before or during .play) – but composed-in cleanups should not resolve at this time. How do you tell the difference? B/ Composed-in cleanups should resolve at .stop time, but other composed-in operations should not. (Sorry if you’ve answered this already – I don’t see it.)

I’m wondering if it would be simpler/clearer to take the idea of “cleanups stored in every event,” but just use an array under a specific key. Currently we use arrays in specific keys to represent cleanup deltas, but an array in a specific key could just as easily represent the total of all cleanups that have to be done. So, e.g., Pgroup would stash its cleanup into that array before passing the event down, and children could add more cleanups before yielding.

I suppose a cleanup event could mark itself as such with some flag – but this “bear of very little brain” doesn’t, at this stage, grasp why a complex, nested, multi-layer Event structure is preferable to a simple array. One rule of thumb is to prefer the simplest implementation that gets the job done – so what is the job that event composition does that an array of cleanups doesn’t handle?

I hope I’m not stepping on toes (anymore) – I think this is an interesting way to simplify cleanups, but I’m not sure I’m seeing the whole justification for event composition here.

There’s a way to obtain the exact same behavior with functions, of course, but it needs a bit more packing, i.e. the actual cleanup function has to be the return value of another function…

I just realized, Thunk is already an idempotent function in SC. So you don’t need an extra layer of function wrapping, nor a routine (so the funny business of hacking Routine’s environment is not necessary at all, actually). Just Thunk { ... cleanup ... }.

hjh