Interface (object oriented programming)

I’ve a orienting question: does sclang has some kind of interface type?

No.

The issue here is doesNotUnderstand.

When you call 1.0.foo, you are first checking if the class Float has the method foo — lets say it doesn’t —
then (and this is the reason why you can’t have interfaces) it calls the method 1.0.doesNotUnderstand(\foo), allowing the called class to decide whether or not to error.

This means you can (or rather should be able to) create a class that is accepted by all functions if the only method you implement is doesNotUnderstand, making it impossible to reason about whether the code will work at compile time or even run time, without actually running the code.

This is a part of smalltalk, its very powerful, particularly for writing wrapper classes (like loggers).

Logger {
   var held, logger;
   doesNotUnderstand {|sel .... args| logger.append(sel); held.perform(sel,  args) }
}

Instead supercollider seems to have adopted the practice of adding a method into Object (or some slightly higher class) called asMyInterface — which lead to a lot of bloat and confuses interfaces with conversions.

So one could create a own custom ‘interface’ type maybe, a convention where a class must have certain methods and a convention about what to do when the method doesNotUnderstand is called (handle as error).

There are a few strategies employed in supercollider.

Duck typing: don’t tell the user what the interface is and hope they get it right. This is particularly bad when you store the object and use it later because the error can happen at anytime (even during a performance).

Inheritance: but without multiple inheritance a class can only ascribe to one interface.

Convert to an object that implements the ‘interface’: hence the many as* methods. This only requires one conversion function. This is sort of like rust’s traits.

The issue with doesnotunderstand comes in when you try to test whether a class adheres to the interface, it isn’t possible from the class definition, only the runtime behaviour.

1 Like

Practically, SC does have interface types - these are more by convention then by the syntax / semantics of the language itself - usually “interface” methods in classes are denoted by throwing a subclassResponsibility error. For example, the Number class specifies some basic operators, but requires that subclasses implement them:

	+ { arg aNumber; ^this.subclassResponsibility(thisMethod) }
	- { arg aNumber; ^this.subclassResponsibility(thisMethod) }
	* { arg aNumber; ^this.subclassResponsibility(thisMethod) }
	/ { arg aNumber; ^this.subclassResponsibility(thisMethod) }
	mod { arg aNumber; ^this.subclassResponsibility(thisMethod) }
	div { arg aNumber; ^this.subclassResponsibility(thisMethod) }
	pow { arg aNumber; ^this.subclassResponsibility(thisMethod) }

There’s of course not a hard guarantee here, but again - this is the conventional way to denote an interface. Interfaces are extremely useful to communicate what kinds of functionality a subclass needs to implement, so making interface base classes with a bunch of subclassResponsibility errors is a pretty reasonable way to go.

1 Like

So this.subclassResponsibility doesn’t work when you choose composition over inheritance, right?

It’s supercollider’s way of marking a virtual or abstract function, but without the guarantee that the method will be implemented.

I’m not exactly sure what you mean here - but if you wanted a composition approach over inheritance (and, because SC is single inheritance, this ends up being what many classes have to use anyway…), and you wanted SOME kind of hard or soft, or at least documented contracts, I could imagine a pattern like this:

SomeClass {
  var delegate;
  *new {
     |delegate|
     delegate.assertMatchesInterface([\foo, \bar]);
     ^super.newCopyArgs(delegate
  }

  foo { |...args| ^delegate.foo(args...) }
  bar { |...args| ^delegate.bar(args...) }
}

However, strictly speaking, any object can dynamically respond to any message because of doesNotUnderstand - so there’s realistically no way of asserting that an object DOESN’T match an interface unless you know that it doesn’t have a custom doesNotUnderstand method. There are some design things that could be done in sclang to make this a bit better.

FWIW I could also imagine making mock “interface” classes that are not usable, and are only used for matching against. For example:

DelegateInterface {
   foo { |a, b, c| }
   bar { |a, b| }
}

A stub class like this provides enough information that you could compare it to any other class and verify that it at least implements the required methods and required arguments. Then, you have something more like delegate.matchesInterface(DelegateInterface).

I think with some caching, this would actually be performant as a runtime check, but I’m not really convinced it would be that useful unless you were building a very large, complex quark with a bunch of very interrelated classes. The fact is, these kinds of interface contracts are generally meant to be compile-time checks, and are mostly useful if they generate a compile error, which is not possible now.

1 Like

You’re going with composition over inheritance in those examples, except this.subclassResponsibility which may work better for regular inheritance.

You people were thinking something like this? Args only work in an Array, it’s yet another case it can happen.

Calculator {
	add { |a, b| ^(a + b) }
}

Logger2 {
    var <>delegate;
    *new { |delegateArg| ^super.new.delegate_(delegateArg); }
    doesNotUnderstand { |sel, args|
        "--> Call to: % with arguments: %".format(sel, args).postln; 
         ^delegate.performList(sel, args)
    }
}

/*
var calc = Calculator.new;
var loggedCalc = Logger2.new(calc); // Wrap the Calculator with Logger2
var result = loggedCalc.add([3, 4]); 
result.postln; 

output:

Intercepted call to: add with arguments: [ 3, 4 ]
7
*/

Both Haskell (to pick a different approach, rust kind of mix the two, right?) and SC lean towards composition rather than inheritance, in principle, but they do it in their unique ways. Haskell mixes and matches functionality using modules, ADTs, and type classes. SC, on the other hand, tends to stick with object composition and doing things on the fly and runtime checks.

SC’s way of dealing with messages, like catching a method call that an object doesn’t recognize with doesNotUnderstand… It’s like, an object can suddenly decide, “Hey, I actually can handle this method,” which sort of works if you’re trying to check interfaces straightforwardly.

I get a bit lost with complicated class structures and this kind of intuitive fly-by-the seat-of-one’s-pants-like polymorphism. Things can get complicated, in my opinion.

And then you’ve got Haskell features like Type Classes with Functional Dependencies, super handy for those times when one type in your type class is gonna dictate what the other type is.

something like…


class (Monad m) => MonadState s m | m -> s where ...

class Foo a b c | a b -> c where ...

or…

class Convertible a b | a -> b where
  convert :: a -> b

instance Convertible Int String where
  convert = show

instance Convertible Bool Int where
  convert False = 0
  convert True = 1

instance (Convertible a b) => Convertible [a] [b] where
  convert = map convert

It’s pretty slick how you can set up these relationships between types and get Haskell to do the heavy lifting, and it is not limited to more similar class instances (type and number or arguments etc).

In SC you could do something inspired by that, but you have to code all the checks yourself. If you don’t document this, it can become very confusing (in my opinion)

1 Like

Well Haskell is object orientated. It just flips things around so that functions are primary rather than objects.

It’s a shame that the computer industry fixated on classes, rather than messages, from Smalltalk. C++/Java don’t have much connection to his vision, or what made Smalltalk great. SuperCollider also missed a trick when it made classes something you have to compile. In smalltalk you can change everything on the fly. If you get a ‘MethodNotDefined’ exception, then the debugger gives you the option to define the method and rerun that function a second time with the same stacktrace.

This is a nice article on what Alan Kay had in mind:

3 Likes

That is a nice article, I think this quote and bit in bold explains why interfaces are not desired in Smalltalk.

In other words, you don’t execute code by calling it by name: you send some data (a message) to an object and it figures out which code, if any, to execute in response. In fact, this can improve your isolation because the receiver is free to ignore any messages it doesn’t understand. It’s a paradigm most are not familiar with, but it’s powerful.

Interesting talk, completely different view to what I normally hear and I can’t follow him on everything tbh. He might be ahead of his time. It could help to gain some more appreciation for sclang, when understanding the background a bit better.

Interesting, the link with his biology background.

“Arrogance is measured in nano-Dijkstra’s.” lol. :slight_smile:

1 Like

He was one of the hippies/hackers/artists in computing before the military/corporate “computer engineers” took over the professional ideology. The irony is that nothing less than JAVA took over and reshaped OOP principles and practices in this new cultural environment. Another irony is that in the '70s and '80s, real engineers were working with computers, which changed a lot in what today is ‘computer science’. Jay Sussman talked about it when someone asked why MIT abandoned Scheme and adopted Python, also changing the entire curriculum.

EDIT: Probably the words ‘engineer’ and ‘scientist’ have different connotations in US academia than elsewhere, I apologize if it sounded weird. To give a counter-example, in Brazil, the title “Engineer” is regulated by the state through the Ministry of Education and other civil organizations, and that means that some universities offer bachelor’s degrees in “computer engineering” and “computer science”. That was the case in my first university, one of the leading ones in the country and always mentioned among those international ‘rankings’. They adopted the “middle” way there. The difference is basically that “engineering” needs to meet a series of requirements, such as more advanced calculus (BTW how strict is the training for CS undergrads in the USA? They go all the way deep into the good engineering and math stuff?), and many other things, so in a way both models were coexisting and mixed there.