Why is (Object's) composeEvents only redefined in Environment but not in Dictionary?

(foo: 2).next((foo: 1, bar: 2))
// -> ( 'foo': 2, 'bar': 2 )

// in contrast

(foo: 2).next(IdentityDictionary [\foo->1, \bar->2])
// (foo: 2)

That happens because Event.next is done as the reverse of composeEvents, i.e.

e1.next(e2) == e2.composeEvents(e1) // for e1 an Event

composeEvents is defined in Object as a no-op that returns the e1, so an e1.next(X) will give back e1 unchanged “for most X”.

(foo: 1).next(42) //-> (foo: 1)

The only class that redefines composeEvents is Environment as this.copy.putAll(arg).

My question is basically: why is Environment treated differently from IdentityDictionary and below in terms of composeEvents, even though those also have a putAll? There’s probably also a “typing” issue involved because

(foo: 2).next(Environment [\foo->1, \bar->2])
// -> Environment[ (foo -> 2), (bar -> 2) ]

So that’s not Exactly and event you get back there which may also be a little surprising. I thought that Environment (unlike IdentityDictionary) might have know = true (like Event) but I see that’s not actually the case…

e = (foo: 2)
e.foo // -> 2

e = (foo: 2).next((Environment [\foo->1, \bar->2]))
// -> Environment[ (foo -> 2), (bar -> 2) ]

e.foo // err

So that couldn’t have been the deciding factor where to redefine composeEvents in the hierarchy…

Also IdentityDictionary has .parent capability too (it’s defined there), just like Event and Environment. So that seems to be another non-factor.

Semantically, a.next(b) means that b is a container, into which stuff from a will be put – anEventStream.next(anEventPrototype).

(From that perspective, (foo: 1).next(42) should fail, because the argument 42 is no sort of container – there is nowhere to put the foo: 1 stuff, so the operation can’t be done. Effectively what it does now is to fail silently – dodgy. Fixing that would be a huge breaking change and I’m pretty sure we won’t do it, but technically Object:composeEvents should throw an error.)

So this seems counterintuitive, but IMO it makes sense: the container you’ve provided is an Environment, and the result is an Environment.

(foo: 2).next(Environment [\foo->1, \bar->2])
// -> Environment[ (foo -> 2), (bar -> 2) ]

So that couldn’t have been the deciding factor where to redefine composeEvents in the hierarchy…

composeEvents was added 15 years ago, at a time when code review was essentially nonexistent. It’s entirely possible that there simply wasn’t a deciding factor: that it seemed at the time like this was the place to put it. That is, a simple and honest mistake.

So the question becomes: Is there any reason not to move composeEvents to Dictionary? Probably that would be fine. At that time, it would be good/essential to add documentation.

But, composeEvents is, AFAICS, unused. It’s not at all a common idiom (which would explain why nobody in 15 years noticed the inconsistency) – and why would it be? Something used as an event stream that overwrites potentially variable data with constant data – viewed this way, it’s unsurprising that it’s not often used. If we deprecated composeEvents, I’d predict very little fallout.


1 Like

Not called directly except by Event.next. But Event.next is used very time one passes an event as-is for a stream, e.g. Pdef does (pattern <> event) that also does an event.next(inevent) inside the Pchain. It would be interesting to test Pdef like that, i.e. when inevent is an Environment.

Aside: there is some related stuff that’s not actually called in the classlib: transformEvent, which is the zero-copy version of that. (And oddly it’s documented on the page for Nil) There’s (now) misleading comment in

Dictionary {
	// Pattern support
	transformEvent { arg event;

I realized that after posting – you’re too fast :laughing: I didn’t have a chance to correct myself.

So yes, we can’t get rid of it – but I think my point stands, that basically nobody is using Event:next directly because there’s very seldom a reason to.


The reason I looked at this is that I was thinking what would be a minimally user friendly implementation of <> for Event (nothing fancy). Presently there is no <> at all for Event (on the left-hand side), but

Event() <> Pattern()
// and
Event() <> Stream()

would make sense since the commuted versions exist with a non-trivial implementation. On the other hand if Event.<> did simply Event.next, things can get pretty odd… and I don’t mean simply that () <> 43 is rather nonsensical (and that 43 <> () is also not defined)… but also in terms of implementation for the stuff that does makes sense, e.g. <> with a Stream:

(foo:2).next(Pbind(\foo, 1, \bar, 1).iter)
// -> ( 'foo': 2 )

Does not include bar into the result because it’s really calling into Object.composeEvents. What we really want is

~basicFriendlyCompose = {|event, other
	case { other.isKindOf(Stream) } {
		Pchain(event, other).iter
    } // more cases

But the next question is when that should actually return an immediately composed Event, e.g. () <> (). Simply

	// looks like an Event, Dict etc. and is not a Stream
	/*case*/ { other.respondsTo(\putAll)}  {
		event.next(other) // == composeEvents(other, event)

turns out is probably a bit iffy for the reason I mentioned above (result can be “less than” an Event in the Dictionary hierarchy). So maybe Event() <> Environment [] should “up-cast” the result to an Event. (And maybe Even.next should do that too for Environment or even Dictionary–after actually composing with the latter.)

The Event() <> Pattern() giving a Pchain case is obvious. But even there, everything has a asStream so oddly one could use anything as a “pattern”. Pchain((), 34) is not an error, but quite questionable semantically, so maybe () <> 34 should probably still return nil since the commuted version is an error anyway. (More oddly, one can presently do 34.asEvent.next(()), but at least that’s an explicit conversion.) So the 3rd/final case should probably be

/* case */ { other.respondsTo('<>') }
{ Pchain(event, other) } /*else nil or maybe throw exception */

only as restrict the “nonsensical” compositions that Pchain can do with anything other since everything has asStream defined…

Well, that errors:

Pdef(\xmm).envir = (dur: 0.2)
Pdef(\xmm).source = Pbind()
// Pdef(\xmm).play(protoEvent: (degree: 3)) // that's ok
Pdef(\xmm).play(protoEvent: Environment [\degree->3])

// ERROR: Message 'synchWithQuant' not understood.
// RECEIVER: Environment[ (degree -> 3) ]

Although I’m a bit miffed about the error since I expected the result to contain more stuff in that Environment.

Pchain(Pbind(), (dur: 0.2)).iter.next(Environment [\degree->3])
// -> Environment[ (degree -> 3), (dur -> 0.2) ]

But Pbind.play fails the same way

Pchain(Pbind(\ctranspose, 4), (dur: 0.2)).play(protoEvent: Environment [\degree->3])
//The preceding error dump is for ERROR: Message 'synchWithQuant' not understood.
// RECEIVER: Environment[ (degree -> 3) ]

It turns out that EventStreamPlayer does this before doing anything else, play-wise

		event = event.synchWithQuant(quant);

So that kinda explains why composeEvents with non-event stuff wasn’t noticed as you can’t get that far in play anyway.

Another point of (my) confusion was that this seems to work as far as composition goes:

(Pbind(\degree, 3) <> Environment [\dur->0.2]).iter.next(())
// -> Environment[ (degree -> 3), (dur -> 0.2) ]

(Pbind(\degree, 3) <> IdentityDictionary [\dur->0.2]).iter.next(())
// -> IdentityDictionary[ (degree -> 3), (dur -> 0.2) ]

But not you actually try to play it, of course, since there’s play for Environment or IdentityDictionary. Thus even composition of Patterns with non-Event Dicts on the right-hand side (where it’s defined already) isn’t too useful; it can do data processing, but the result is not playable unless explicitly converted to Events. So, if instead of the Pbind there’s a simple Event on the left-hand side in those last examples, it probably doesn’t matter much what’s returned.