Runtime 'class' — ProtoObject

There are numerous issues with using Event as the base for object prototyping / runtime ‘classes’.

I’ve made a PR sclang, classlib: ProtoObject, a better runtime 'class' implementation by JordanHendersonMusic · Pull Request #7219 · supercollider/supercollider · GitHub that proposes a fix.

The class is called ProtoObject right now, but it doesn’t do object prototyping, instead it does single inheritance and allows for the use of doesNotUnderstand. The implementation of ProtoObject, except from the syntax, is done completely in supercollider use the new AbstractObject sclang: introduce AbstractObject by JordanHendersonMusic · Pull Request #7205 · supercollider/supercollider · GitHub, meaning other people may wish to create their own runtime class-like structures, so this doesn’t have to work every single use case. This is why I haven’t actually implemented object-prototyping, because although it is very powerful, most of the time, people just use it to implement classes (see javascript and the class keyword). This means there is no proto or parent which you get with Event.

This is a big proposal so I’ve duplicated the post here to get more eyes on it.

Here is what it looks like to use.

I’d love any thoughts or feedback!

Same as event, slightly different syntax with #(...).

a = #(meow: 1);
a.meow \\ 1
a = #(numChannels: \meow);
a.numChannels \\ \meow --- with event this would have returned 1
a = #(meow: 10, protoObjectLock: true)
a.woof = 10; // Prints error

Create keys at runtime.

a = #(meow: 10)
a.woof = 4;  
a.woof // 4
a.meow // 10

Lock object, no new keys once created.

a = #(meow: 10, protoObjectLock: true)
a.meow // 10
a.woof = 10; // Prints error

Inheritance is done by assigning to super.

parent = #(speak: {|self| "I say %".format(self.word) });

base = #(super: parent, word: \meow);

base.speak // I say meow

Can overload doesNotUnderstand

a = #(doesNotUnderstand: "I'm sorry Dave, I'm afraid I can't do that."} );
a.shutdown //  "I'm sorry Dave, I'm afraid I can't do that."

Value can be overloaded.

a = #(value: \meow );
a.(); // \meow

Can overload how the object is printed to the post window.

a = #(asString: \meow ); // meow

You can also use this to create exception-like objects.

7 Likes

All of this looks great to me.

Wow this seems like a great move forward! first random thoughts and questions:

can we overload “printOn” rather than using “asString”? …and what is printed if we don’t - “a ProtoOject” or are these simply "Object"s ?

I would like to be able to evaluate functions in the protoObject’s environment so: {~myVar} instead of {| self | self.myVar} - like we do in an eventType’s play method… I make Classes sometimes just to avoid ‘self’.

I’m trying to think of things that native classes can do which can’t be accomplished here - is there any way to do any initialization on instantiation or do we have to call a method? you’ve got protoLock would there be any point in a protoInit for parents?

is there any advantage to native classes performance-wise?

if adopted should we not deprecate the prototyping functionality in Event?

the syntax is an important decision - at first blush this seems elegant - will be easy to type and see…

only possible thought is that we are using # for literal Arrays and for multiple assignment. One alternative might be ^(foo: \bar). Either is fine though!.

ddwPrototype (the quark) could be changed to inherit from the new abstract object.

While I, too, disagree with the self business, I understand why it’s there and don’t feel that it’s necessary to change it in the core class library, when it’s easy to write my own alternative.

One thing that’s good about self is that, the other way [ddwPrototype way] requires every doesNotUnderstand dispatch to evaluate a function by .use, which involves protect, which means 3 or 4 stack frames for every level of dispatch. When the stack gets very deep, this can break. Now, by “very deep,” I mean an earlier version of my cll parser, which needed to handle arbitrary nesting depth in the input strings. Most use cases never hit that limit. But it’s good to be aware at least that treating ~envVars as local to the proto object does come with a performance cost and a practical limit, and it’s good that users can choose which way is appropriate for their use case.

hjh

Yes! The empty proto object has a bunch of built in methods - without them the actual of printing (and other things) would crash the interpreter. You can override all of these (including class) bar a select few.

A protoobject is not an environment or event. You can’t do this. Essentially there is too much infrastructure built into environment to make it a good choice for the underlying container.

That being said, you could create your own ProtoEnvironment. ProtoObject is only 100 or so lines long.

This is not possible in sc3 (as this is defined in all context!?), but thinking towards sc4 (which I have started giving serious thoughts to recently) it would be possible.

Huge one. The goal here is complete and utter flexibly. Now it could be improved with some primitives, but that isn’t the goal just yet.

While I am perhaps more open to breaking changes in even core language features, they should only be done if something is compete broken (having a ghost conversation about resondsTo right now…) — I don’t think event prototyping is completely unusable, just hard to use safely and too rigid, it also solves a slightly different problem - so it should stay.

So this is an object prototyping system meaning there are no init methods… Instead you should make a function that returns a protoobject instance. When you assign to super you are assigning an object instance, not template to create an object.

I may also consider adding a mixin system which would let you combine proto objects.

The issue is that ^( ... is already valid SC syntax - it is returning a parenthesised expression.

All new syntax must be previously invalid syntax or run the risk of having language issues.

This approach to abstract object was the goal!

AbstractObject is not a complete Object it will crash the interpreter in many cases… The user gets to decide how to implement these necessary core features, which brings a lot of flexibility.

1 Like

I believe you can do this if the proto object implements envirGet and envirPut. My Proto class doesn’t inherit from Environment but I’ve been using it for env vars for a couple decades now, by implementing these methods.

There is nothing special about Environment that makes it the only possible provider of environment variables. ~myVar is only a shortcut syntax for \myVar.envirGet. There’s no infrastructure beyond this.

hjh

1 Like

Hmm, I’ll have a look again at this then, thanks!

1 Like