Getting right to the point… TL;DR I think efforts to extend the features of SynthDef should favor “has-a” composition rather than “is-a” adding methods (or inheritance). But I can imagine different opinions about the boundaries, so I thought it might be worth starting a conversation.
This is motivated by making Synthdef work in many threads by JordanHendersonMusic · Pull Request #6073 · supercollider/supercollider · GitHub, which changes UGen.buildSynthDef from a single global value to a thread-local value. Just on a technical level, that’s a reasonable idea; it’s bothered me at times that
UGen.buildSynthDef is this unprotected global thing hanging out there, in a critical core area. (I don’t have an opinion on the implementation; first glance looks pretty good! Just that making it thread-safe(r) would be a win.)
But the other given rationale – “Further, in my own work I often want to make [an] owned resource as a part of the synthdef and synth creation process” – I have doubts, and the PR conversation thread wasn’t the right place for that.
We have a server abstraction object representing a server-side GraphDef, just like we have server abstraction objects representing Groups, Nodes, Buffers and Buses. GraphDef <–> SynthDef. Opinion: I think it’s necessary to have a language-side object corresponding closely to GraphDef.
If that’s a correct view, then there would be three ways to extend SynthDef.
- Push the GraphDef companion down into a lower-level class with a different name, and redo SynthDef as a wrapper around that.
- Or, add features to SynthDef, ditching some of the SynthDef <–> GraphDef correspondence.
- Or, keep SynthDef as it is, and build superstructures on top of it.
It seems pretty clear to me that 1/ is intrusive and risky.
With 2/, it’s more of a matter of opinion, but I think this idea breaks encapsulation. SynthDef already has a large and complex job; it doesn’t need additional complex jobs added into the logic. (That it’s complex is an argument in favor of encapsulation.)
3/ preserves backward compatibility and encapsulation. For me, this is convincing.
So an alternate solution to the stated requirement would be, instead of managing resources from within SynthDef, to create a third party that manages resources and builds SynthDefs, in the right order. I think the intention behind managing resources in SynthDef is convenience, but even a halfway well written resource+SynthDef agent would be just as convenient, and easier to maintain (because everybody has a clearly defined job). This is “has-a” composition.
Loading resources in a SynthDef has that “wouldn’t it be cool if…?” factor – the thrill of seeing SynthDef from a different angle. But this doesn’t ensure that it’s optimal. Considering changes to the core classes on the basis of personal approaches that may or may not be optimal is potentially risky – and SynthDef seems particularly prone to this temptation (probably because it’s the only server abstraction object that executes a user function in normal operation).
In the present case, however, there are other good reasons to improve thread safety; I don’t object to those at all. And of course, a thread-safe(r) SynthDef could be used however one likes. Part of my concern, though, is what we recommend as a best practice; IMO, clear(er) lines between server and client operations are easier to explain to new users, and “has-a” composition supports this better.
Or am I off-base here? I don’t think so…? But maybe there are cases that I’m not seeing.