Sclang evolution, and influency of modern languages on its development

Just curious to know if devs and users still have similar opinions about it, and if that may be part of a future major version update. Or if we don’t even have the resources to think about it,

Continuing the discussion from SCLang Architecture / random questions:

I’d actually prefer a role/traits based system and move away from inheritance altogether… but that’s a different language.

In sc, the only thing I want is red squiggles when I do something wrong.

I think there would be an challenge in that Smalltalk uses wrapper classes with doesNotUnderstand and relies on being able to reroute messages at runtime, I imagine that makes implementing a structural type system or even interface based approach challenging because any object could potentially wrap any other object. You’d need to verify whether an object responds to a message and doesn’t throw a DoesNotUndersrand error.

I think an explicit type system would actually be quite annoying to many users.

2 Likes

A crucial question for a hypothetical type system in SuperCollider is operator overloading (or method/function argument type overloading).

SC is designed for easy overloading, so much so that it’s easy to miss just how much overloading there is, and how transparently we can exploit it.

My usual example for this is a pitch-processing function that should accept either a single pitch as a number, or a chord as an array of numbers. In SC, you can write one function that dynamically dispatches down the right code path, without any intervention or hinting or code duplication in user code. A Python @dispatch seems to call for a separate function definition for each combination of input types. If SC required that, I’d agree with Jordan that this would get very old very fast. (Three inputs, all of which can be either arrays or single values → eight function defs… yeeeeah I’d rather not.) Some language might have a clever approach to this, which I don’t know about (bc there’s a lot I don’t know about) – I’m just saying that the Python dispatch keyword would be terrible for SC.

There are a couple of reasons for strict(er) typing. One is compile-time validation, which would be easier to handle even with dynamic dispatch. The other is compile-time binding, which would require extremely careful design so as not to lose flexibility. Here “I Am Not A Computer Scientist” so I won’t dare to speculate.

hjh

2 Likes

Thank you for the feedback, @jordan. Alexis King’s article is very cool to explain this topic. They explain this in a way that is both very simple and also touches on fundamental points that are almost always misunderstood. In a half-backed implementation, it is quite annoying. I Agree!!

But the rabbit hole goes way deeper.

I will quote their points verbatim:

Instead of thinking about how to restrict, it can be useful to think about how to correctly construct.

In Haskell, datatype declarations invent new values out of thin air.

We can represent a lot of different data structures using the incredibly simple framework of “datatypes with several possibilities.”

Any statically typed language could be worked out to be more like this. If this is achieved, it will be clear there is no kind of “restriction”. You can do all kinds of overloading and you can even “play god” with your code. That’s the tricky part people don’t agree. Types can be used not just to prevent invalid states but to define what makes a state valid in the first place. This is subtle but changes everything.

For reference:

https://lexi-lambda.github.io/blog/2020/08/13/types-as-axioms-or-playing-god-with-static-types/

https://lexi-lambda.github.io/blog/2020/01/19/no-dynamic-type-systems-are-not-inherently-more-open/

1 Like

Depending on how you think about it, sclang already has a limited, ad-hoc implementation of type-based dynamic dispatch. You can imagine the collection of all methods defined in SuperCollider as a map between a tuples that look like: <Symbol, Type, Type, ...Type> and a method implementation. SuperCollider does runtime dispatching based on only the first two in the tuple, and all the rest are assumed to be Object (e.g. derived from Object, which is just any value at all). So, a table of play methods might look like:

{
   <'play', Ndef, ....Object>: { ...Ndef:play method... },
   <'play', Pdef, ....Object>: { ...Pdef:play method... },
}

Secondly, sclang does another limited form of runtime dispatching with unary and binary operators. The primitive implementation of these look like this (in pseudocode):

// operatorPlus,  left.type == Integer
if (right.type == Integer) {
    plusIntInt(left, right); 
} else if (right.type == Float) {
    plusIntFloat(left, right);
} ... etc ...

This is just the same as:

{
   <'plus', Integer, Integer>: { ...int+int implementation... },
   <'plus', Integer, Float>: { ...int+float implementation... },
   <'plus', Float, Integer>: { ...float+int implementation... },
   ... etc ...
}

It’s not so hard to imagine typed dispatch bolted onto the existing sclang runtime. We keep the current “big table” dispatching for the symbol + first argument, and then dispatch on the remaining arguments in the same way we do binop dispatching (in pseudocode):

for (auto method : possibleMethods) {
   if (signatureMatches(method, argumentTypes))
   {
       invoke(method, arguments);
   }
}

All existing sclang methods work fine with an implementation like this, since they can be assumed to have a signature that expects all Object’s (e.g. they take any input) - cases where there are methods that overload based on arguments beyond the first are just cases where possibleMethods.size > 1.

Practically, this would do two things -

First, it would remove the need for most type checks in sclang, as well as most type checks in primitives. Rather than writing:

doSomething {
   |what|
   if (what.isKindOf(Array)) { 
      what.do { |value| value.something } 
   } {
      what.something;
   }
}

you could write this:

doSomething {
  | what:Array |
  what.do { |value| value.something } 
}

doSomething {
  | what:Object |
  what.something;
}

Dispatch now takes care of branching based on the type of what.

It’s worth noting that, while this kind of dispatch seems like it would be slow, it’s guaranteed to be no slower than any of the type-based dispatching we do NOW (which are, in the end, just doing some form of type comparisons in a sequence of if-else blocks). FWIW the nim language, which does dispatch similar to what I’m describing for sclang, is iirc still just doing if-else checking for all the types past the first one - there’s not any particularly magical optimizations going on here.

This kind of dispatch could even be implemented purely as syntactic sugar, with no runtime changes required at all - we can simply compile the two separate overloads of doSomething in the previous example back to the old school single implementation with branching isKindOf checks. In this case, we have all the functionality of typed dispatch for every argument, with more or less the same performance as we’d get now for comparable code.

There’s a whole pile of optimizations that can be done with a system like this - actually, many of these optimizations are things sclang is already doing, just not in a systematic way. For example: control flow is not a fundamental feature of sclang - it only get control flow via dispatch. “If” branching is basically implemented like this:

+True {
  if {
    |trueFunc, falseFunc| 
    trueFunc.value()
  }
}
+False {
  if {
    |trueFunc, falseFunc| 
    falseFunc.value()
  }
}
+Object {
  MustBeBooleanError().throw;
}

In a subset of common cases, if blocks are inlined - this effectively ends up looking like:

if (condition.isKindOf(True)) { 
  trueFunc.value() 
} else if (condition.isKindOf(False)) { 
  falseFunc.value() 
} else {
  MustBeBooleanError().throw // implicitly condition.isKindOf(Object)
)

In short: the sclang compiler is just inlining the dispatch code for efficiency. Generalizing this optimization would mean we could potentially apply it to other similar cases across the language (for example, maybe this could be applied to ANY case where a method name has 2 or fewer implementations) - it would also eliminate a bunch of special case optimization code for control flow structures like if and while.

4 Likes

Just scratched what I was thinking…

So why bother with it? One reason among many is the possibility of thinking at the level of types, and “types of types”.

(Type is not given by the language, but are your propositions)

But it can get messy in some cases. When you dive into how different types work in programming, you quickly see that some types are way more complicated than others. Take lists, for example. It would be a huge headache and super clunky if you had to have a different type for every kind of list out there—like one type for lists of numbers, another for lists of true/false values, and yet another for lists mixing those thing.

Now imagine that with sclang’s Arrays. Each structure of an Array would be a type. To proper reason about an algebra of arrays, you would need to think on a type level too.

It would be super fun to think about a dynamic Array operation implemented in a statically typed language. (I kind of am trying this with rhythm trees, and extracting their structure as a value etc. Those are arbitraryy types of arrays, so each one is actually a type, but treated as a “value” etc)

Instead, most programming languages keep it simple and just give you one flexible List type, or Array type.

But, in more structure languages (not sclang), this List type isn’t meant to fly solo. It’s supposed to team up with another type, and that’s how you end up with specific kinds of lists, like a list of numbers (List Integer) or a list of texts (List String).

Also, in some programming languages, they make functions that can deal with any and every input you throw at them, which is what they mean by a function being “total.” Like, if you have a simple function that flips a true/false value, it’s considered total because it can handle whatever boolean value you give it. This helps coders understand their code just by looking at the types; if you’ve got a function that turns type A into type B, you’re guaranteed to get a B out of every A.

That’s another instance that one can reason on a type level.

But, making a total function isn’t always doable. Sometimes, the language doesn’t give you the right tools to describe every possible input, or it’s just too hard to pin down exactly what inputs your function needs. That’s when languages let you make “partial” functions, which don’t cover every imaginable input. Partial functions can be handy, but you’ve got to be careful with them to avoid crashing your program if it runs into an input it can’t handle.

(I will elaborate better, I just saw your post arriving, so I will read it now)

Because True and False are different types in sclang, this kind of dispatch even lets you remove branching from your code, in favor of dispatch. This code:

playing_{
  |bool|
  if (bool) { 
    this.play() 
  } {
    this.stop()
  }
}

Could be rewritten as:

playing_{
  |bool:True|
  this.play()
}
playing_{
  |bool:False|
  this.stop()
}

In an ideal world, both of these would be optimized to the same bytecode - it’s interesting to imagine the sequence of optimization rules that it would take to derive the second version from the first one.

2 Likes

My mistake, I should have written ‘I think an explicit type system would actually be quite annoying to many users’. Code where the user doesn’t have to explicitly mention types, I agree, would be amazing.

1 Like

That’s all interesting to think about sclang.

One thing that people don’t realize, I think, is that a “statically typed language” like Haskell has no types defined in the compiler! Only numbers come with the compiler (GHC). Everything else is defined in ordinary modules (base) even those you don’t need to import.

When you define a type, it is equivalent to a proposition in logic (and functions are proofs of that proposition). So it makes sense to define and reason about types like you draw a UML (those dyagrams etc), and not be restricted to what the language gives you.

(The way they do it, is because there is an intermediate language (also statically typed), and ‘Haskell’ is a lot of synthetic sugar for that one. They are pretty serious about lambda calculus)

(will reply a bit later)

Yes. The genius of Smalltalk is that there’s no need for control structures. All branching and looping can be handled by dispatch – perhaps not with optimal performance, but in principle, hardwired compiler control structures are totally superfluous in this scheme.

hjh

1 Like

SuperCollider would only really need user-specified types on function arguments and class members. And, many cases where you would write these, it would be to replace a type check that’s currently being done in user code (e.g. a few of my examples from before) - so it’s not actually less / simpler code, only a different syntax to express a similar concept.

We could e.g. allow type specifiers on local variables:

{
  var i:Integer;
  i = 4;
}

But this would probably just be equivalent to something like:

{
  var i;
  i = Integer.checkType(4); // return value if it's an Integer or throw a type error.
}
1 Like

Doesn’t this tie everything into inheritance though and negate duck typing?

This was my original point that ideally you wanted to be able to say something like…

{ |foo : HasMethod(\doSomething, Number) | foo.doSomething(1.0) }

Now you can compose classes and implement methods rather than strictly inherit from a type to get be accepted into a function.

Could this be done in supercollider?

1 Like

I’ve been looking quite a lot at how different languages implement runtime dispatch, especially with type constraints - it’s pretty much ALWAYS done as a hodge-podge of different layered optimizations for different common cases, with some brute force fall-throughs for the cases that are more difficult. it’s pretty simple to go from a dispatch-based if implementation to a bytecode / machine code representation that is “optimal” branching code.

1 Like
{ |foo : HasMethod(\doSomething, Number) | foo.doSomething(1.0) }

One subtlety here: HasMethod is not a useful constraint for SuperCollider. Why? Because, invoking any method is valid on any object whatsoever. All methods effectively have an overload of the form:
<'someMethod', ...Object> -> { doesNotUnderstand(...) }
(that is, if an exact match isn’t found for a method, we always fall through to Object:doesNotUnderstand).

I think a “HasMethod” constraint actually hurts us! Imagine calling the function you specified:

jordansFunc(fooWithoutDoSomething);

In this case, we would try to find a matching jordansFunc overload - since fooWithoutDoSomething doesn’t match the HasMethod constraint, we have no match, so this gets dispatched to Object:doesNotUnderstand. The resulting behavior, then, is the same with or without the constraint: a doesNotUnderstand call.

But what if we have a custom overloaded doesNotUnderstand, like the Event class? In our “naive” version with no type constraint, we end up with a call like doesNotUnderstand(foo, \doSomething, 1.0) - this gives our class enough information to e.g. route the call to some custom implementation of \doSomething (this is what the Event class does).

But, in the case where we have a constraint, we instead end up with a call like doesNotUnderstand(foo, \jordansFunc) - we’ve lost information. While this MIGHT be useful in some cases, my intuition is that moving the doesNotUnderstand error one level out like this gains us nothing but potentially loses valuable information.

That isn’t to say that there wouldn’t be good use-cases for specifying constraints like this, but I can’t think of one at the moment? There’s a major difference here to something like javascript - a javascript object can have an arbitrary set of properties, and these properties are public (e.g. you can refer to them from the outside) - this makes a constraint like hasProperty('foo') useful. In SuperCollider, an object has a fixed set of properties strictly determined by it’s type, AND these properties are not accessible from the outside, only via methods.

Given the doesNotUnderstand conundrum, it seems like method-based constraints may not be valuable - and, property-based duck type constraints aren’t valuable either, since you can’t access an objects properties from the outside. I’m not sure what ELSE you’d use in a constraint other than these two things?

1 Like

That is what I though.

So the only type system supercollider could support would be a strictly inheritance based system, not a structural type system, nor constraint?

Meaning James’ example …

… would require writing an overload for Number and Collection and it wouldn’t be possible to say…

{|pitch: IsNumeric| ... }

… which matches Numbers, but also any functor of Numbers (Collections, Optionals…).

Does this affect multichannel expansion? Or am I missing something?

1 Like

So the only type system supercollider could support would be a strictly inheritance based system, not a structural type system, nor constraint?

I don’t think there could be a structural type system, because the structure of Objects in SuperCollider is purely internal and hidden. From the outside, every object in SuperCollider is effectively:

  1. A type
  2. A hidden storage array of unknown size.

So the only information we really HAVE to use for dispatch is the type.

1 Like

A quickie about this

So Lists and Arrays are not types, nor a pair of List of Something (or course), they are just slots, right?

I mean: sclang has a type system and offers a lot more flexibility and capability, but it’s hidden. The emphasis here isn’t to just discern the correctness of the code.

(I’m trying to avoid the idea of a mere type-checker, you got it of course)

Lists and Arrays out from the type system appear as a serious compromise. (You agree? )

What if evolving the language in a way that lets us define types and those play nice with the primitive ones? Imagine this making you rethink how you see functions and their interactions. It’s not just about making things correct; it’s about opening up new ways to think and solve problems. (You already showed it’s possible)

And for performance reasons, you can even deal with types until a certain point where they are useful in this way, and then erase them. That is, once your program starts running, all that type of info gets tossed out the window (type erasure), including “what type” of lists, arrays, etc.

(It’s a pretty open question)

EDIT: Also, the idea of type classes (which would mean a fancy type system), is not that mysterious, one can understand it as just functions that at compile time evaluate type info into expressions. And, stretching a little bit, if you can go type ↔ expression, you have a code generation system.

Which isn’t too far from the usual Smalltalk idiom of:

+True { playing_ { this.play() } }
+False { playing_ { this.stop() } }
1 Like

We probably can just honor smalltalk tradition regarding all this. It’s a beautiful model. People write papers about languages to distill the essence, and we should be able to ignore the particulars. The distilled ideas are the things that could have influence. but in the context at hand, of course…

I think a structural type system is not possible, or doesn’t make sense? In the current sclang semantics, the only place where the structure of an object is available is inside a method of objects class - it’s basically always private. To try to think this through (maybe you have some ideas here too) ----

  • An Object is just a storage array of some size, plus a type (e.g. a Class)
  • A Class contains a map from member names to indices in that array (e.g. var foo, bar; might correspond to: { 'foo': 0, 'bar': 1 } - in code where we know concretely the type of an object, we can compile named member accesses into reads of the internal storage array (e.g. bar.postln becomes something equivalent to this[1].postln).

Imagining something like this…

postName(obj : HasName) { // assume HasName means it has a name member
    name.postln; // implicitly refers to a member of obj
}

Lets assume we keep sclang semantics, where members of the first argument (implicitly, this) are automatically “in scope” so you can refer to them directly… What are the consequences:

  1. Since we don’t know the type of obj, we still have to compile into something like obj[obj.class.indexOfMember[\name]]. So there’s no real optimization to providing a structural constraint here (but maybe thats okay).
  2. We have now entangled method resolution for a type with it’s internal storage structure. In other words, if we refactor a type T by renaming it’s name member to canonicalName, then calls to postName(T()) will now be doesNotUnderstand. For starters, it’s quite hard to provide a compile time error for this. And the runtime errors will be hard to understand as well - in this case, renaming an internal storage member would result in something like Method "postName" not understood

Likewise, constraints only make sense on things that we could know about an object at compile time - but an object is just a type plus storage, so there’s not much TO know apart from the type.

I think ultimately this boils down to a fundamental difference between e.g. Typescript/JS and SC: Typescript has anonymous types - that is, it can have objects with storage that isn’t tied to a named type: { "foo": 10, "bar": 20 } - in this case, the only was we can make a typed function that takes this object is with a structural constraint. since we have no other way to refer to it. At most, structural constraints in sclang would let you share one implementation function between all types that have an internal property with some name - but again this feels fragile and seems to make errors etc more obscure?