Reserved method names in Events checked one way, not the other

I was pleased to discover today that if you add a reserved method to an object prototype as below, SC gives you a warning:

e = ()
e.reset = {"foo".postln}
// —> WARNING: 'reset' exists as a method name, so you can't use it as a pseudo-method.

However, if you do the same this way, you get nothing:

e = ()
e = ( reset: {"foo".postln})

That seems like an oversight to me, and would save much frustration if it were fixed. Or is there a structural reason why the interpreter can’t check it when assigned this way?

As a side note: in case it’s useful, both of these methods can help with getting to know which Event method names are reserved:

Event.dumpMethodList;
Event.dumpFullInterface;

One tip I’ve heard is to use underscores in your method names which then (usually) won’t be reserved.
Best,
Paul

SC doesn’t have an object prototype class. (Events “are” not object prototypes.)

If we did have a dedicated object prototype class, then this problem could be solved easily. But you can’t add this warning to Event without breaking other valid use cases of Events (which are named Event, not Prototype).

An Event’s primary purpose is to collect data which are then .played.

Sometime later, the know flag was added to Event’s parent class, IdentityDictionary, and doesNotUnderstand was redefined to support redirection of undefined method calls. Then the flag was turned on by default, but only for Event :face_with_raised_eyebrow: which suggests that Events are meant for prototyping… but they aren’t really meant for that.

When you create an event, the interpreter doesn’t know if it’s made of “just data that will be played” or if it’s made of “data and pseudo-methods that will be called.” The warning makes sense in the latter case, but not in the former – and the distinction is based on future user action. At the time of event population, it isn’t known whether the user will need the warning or not.

Your .reset_ example issues the warning because we guess: if the user is setting the value using a method call, there’s a higher likelihood that they will want to access it later by a method call. But in the case of (reset: ...), there is no indication in this expression of how the user intends to use it.

Consider this case:

p = Pbind(
    \rgb, Pwhite(0.5, 1.0, inf).clump(3),
    \addr, NetAddr("127.0.0.1", 11111),
    \play, {
        // a one-off play behavior
        // which could be added as an eventType
        // but which I'm not reusing, so, nah
        ~addr.sendMsg('/rgb', *~rgb)
    }
).play;

Currently this is allowed. But adding the proposed warning would mean that you’d get warnings with this valid use case.

hjh

1 Like

This is a good idea I may start using more systematically!

Thanks much for this great explanation, @jamshark70. All makes sense within the specific historical context, and I agree with you that it’s not a great design decision. Has there ever been an effort to create a dedicated Prototype class that doesn’t have these restrictions?

Sometime later, the know flag was added to Event’s parent class, IdentityDictionary, and doesNotUnderstand was redefined to support redirection of undefined method calls. Then the flag was turned on by default, but only for Event :face_with_raised_eyebrow: which suggests that Events are meant for prototyping… but they aren’t really meant for that.

How did EventStreamPlayer work before the know flag, given that missing Event keys are looked up in the parent Event? In my understanding, that whole system is based on the idea of prototyping, but maybe I’m missing something…

The know flag doesn’t have anything to do with parent events.

e = (a: 1, parent: (b: 2));

e.know;  // true

e[\b]  // 2, ok
e.use { ~b }  // 2, ok
e.b    // 2, ok

e.know = false;

e[\b]  // 2
e.use { ~b }  // 2, ok
e.b    // Message 'b' not understood.

Access to parent event keys using at, or, if the event is the currentEnvironment, using envirGet / ~envVar syntax, always works. There is no way to use know to stop that from working.

know is used only here, to forward to the error-generator if the flag is unset. So know affects only method calls that are not dispatched to true methods.

	doesNotUnderstand { |selector... args, kwargs|
		var sel, forward;
		if (know.not) {
			^this.superPerformArgs(\doesNotUnderstand, [selector] ++ args, kwargs)
		};

Event usage in patterns has never relied on pseudo-method calls. Patterns put into events; event.play use-s the event so that ~envVar syntax works.

EventStreamPlayer accepts a “prototype” event, but this means only that the event is pre-populated with some values and functions. This usage of the word “prototype” is completely distinct from the concept of object prototyping.

(I don’t think anybody uses EventStreamPlayer’s event: arg, btw, because IMO it’s not well-modeled in the vanilla class library. A pattern’s behavior depends on the pattern and the prototype event and probably on the other resources as well, like buses or buffers. Currently, to use prototype events, you have to remember to pass in the protoevent every time you play the pattern, even though the pattern and the event are in practice tightly coupled. When tightly coupled entities are just free-floating without their relationship being modeled, problems will follow and discourage users from going there. This is why I made PR(\name) as a “process prototype,” analogous to a class, and BP(\name) as a “bound process,” analogous to an instance that’s bound to specific values. The BP unifies pattern, protoevent and resources under one roof. I have used protoevents – just this past week, in fact – because I have a proper architecture to support that usage: ( ... stuff ...) => ProtoEvent(\newThing) and then, in the PR, ~event = (eventKey: \newThing) and then I never have to think about it again, for that process.)

hjh

1 Like

There was a proposal (not really a formal proposal) to change the class hierarchy so that, instead of Object being at the top, there would be a NullObject that basically defines only *new and nothing else. Then object-prototype classes could be derived from this, and implement only the bare-minimum interface. That is, where currently an object-prototype has to avoid every method name that’s defined in Object, Collection, Dictionary, IdentityDictionary, Environment and Event – x = IdentitySet.new; (Event.superclasses ++ [Event]).do { |cl| cl.methods.do { |m| if(m.filenameSymbol.asString.contains("SCClassLibrary")) { x.add(m.name) } } }; x.size says this is 438 method names in the core library, of which about 100 come from Object :flushed: – under this scheme, there might be 20(?) methods required just to support basic lookup, and the rest would be free.

Currently, the interpreter will always dispatch to a “real” method first. This is necessary for speed. The “real” method lookup is constant-time O(1) access into a two-dimensional table, and the great majority of method calls will be correctly satisfied this way. Lookup of ad-hoc methods will not be as fast as O(1), and only a tiny fraction of a percent of method calls require this. To give ad-hoc methods precedence, it would be necessary to do the slower lookup first for every method call, impairing the performance of all method calls even in the vast majority of cases where it isn’t relevant.

So I don’t see any way to improve the situation other than Julian’s suggestion outlined above. We don’t need SC to become an order of magnitude or two slower.

(Also, I have a ddwProto quark, which directly reimplements some common messages like play, stop, reset, next on Proto objects. It’s not a complete solution, but alleviates the pain for some common cases.)

hjh

Ah, I didn’t know that, thanks for clarifying! As someone who is familiar with prototypical languages like JS and Lua, I would have expected that know enables/disables the full prototype chain. I wasn’t aware that it only affects pseudo-method syntax!

Is there a way for me to use your PR and BP classes?

This seems like a terrific idea to me. It doesn’t seem like it would break anything — all existing classes would remain as they are, children of Object — and enabling a genuine object-prototype class would, in my opinion, be a great improvement. Why not go for it?

Been trying to do this today… It is a pain for some reason. The code is fragile around this part of the interpreter.

2 Likes

They’re defined in the ddwChucklib quark. Documentation is minimal :face_with_diagonal_mouth: but I put up a couple of tutorial pages awhile back.

http://www.dewdrop-world.net/sc3/tutorials/index.php?id=6

http://www.dewdrop-world.net/sc3/tutorials/index.php?id=7

Though PR and BP in themselves don’t really address the namespace collision issue that started the thread.

hjh

1 Like

Godspeed! Please keep us posted :slight_smile:

1 Like

Brilliant. I see the challenges involved, but I think this would be a significant step forward for SC.

1 Like

I’ve also added some syntax to make it easier to create these protoobjects.

a = #( asString: "bang!" );
// -> bang!
a = #(
   numChannels: { |self, n| n },
   reset: { "foo" }
);
a.numChannels(10) // 10
a.reset // foo

I’ve also added an AbstractWrapper that lets you create a method logger

a = Debug(\myEvent, ());
//DEBUG myEvent: Before calling asString(args: [], kwargs: []), myEvent = ()
//DEBUG myEvent: After calling asString(args: [], kwargs: []), result = ()
//-> ()

a.foo = 10;
//DEBUG myEvent: Before calling foo_(args: [10], kwargs: []), myEvent = ()
//DEBUG myEvent: After calling foo_(args: [10], kwargs: []), result = ('foo': 10)
//-> ('foo': 10)

I think I have solved all the issues, it isn’t the prettiest code and AbstractObject will create loads of errors if you try and use it directly (printing to the post window is a whole ordeal) so it should be considered as an incomplete object that needs to be subclass to create a complete object.

2 Likes

One thing I love about the #( notation is how easy it would be to convert old pseudo objects to this purer form. You have my full support, for what it’s worth!

1 Like