Multiple inheritance (not class-factory mixins). Will it ever be a language feature?

I wonder if we can do it.

I don't like class-factory mixins, because they require wrapping classes in functions. Therefore, one can not import a 3rd-party class and compose it.

I've implemented a multiple() inheritance helper tool using Proxy under the hood. It works like this:

import {multiple} from 'lowclass'

class One {one() {}}
class Two {two() {}}
class Three {three() {}}
class Three extends multiple(One, Two, Three) {
  four() {
    this.one()
    this.two()
    this.three()
  }
}

That's a simple example, but you start to get the idea.

Implementation(s) are in lowclass/multiple.ts, and basic initial tests in lowclass/multiple.test.ts.

(Curious to know if you have any ideas on how to improve it!)

There are still a bunch of cases I need to add to the tests and account for in the implementation, but it works for simple use cases like the following:

Suppose we have the following code:

import {ExternalClass} from 'third-party-library'

class OurClass extends ExternalClass { ... }

Then, imagine that we want to add EventEmitter functionality to OurClass so it will emit events. We can do this simply:

import {ExternalClass} from 'third-party-library'
import {multiple} from 'lowclass'
import EventEmitter from 'events' // from Node.js

class OurClass extends multiple(ExternalClass, EventEmitter) {...}

const o = new OurClass
o.on('some-event', () => {...})

This is better than class-factory mixins for two main reasons (among others):

  • all functionalities are written as plain classes. No wrapper-function boilerplate code. Simple.
  • import any 3rd-party classes and mix and match them. This is impossible with class-factory mixins.

TODOs:

  • per-constructor args. This will give the ability to pass specific args to each constructor that a subclass extends from.
  • statics: I haven't needed static inheritance yet so I haven't implemented that yet, but inevitably it'll be needed, and that's easy to implement.
  • Use Symbol.hasInstance to make instanceof work.
  • Diamond problem, or similar: give the ability to specify which super methods to call when there are name collisions in a hierarchy.

I am already familiar with proposal-mixins, but I don't like it because it's syntax for class-factory mixins. It means that we must explicitly choose between writing a class or writing a mixin. We get the same problem with 3rd-party code: we don't control the source, so we can't just convert a class into a mixin.

How would true multiple inheritance syntax look like?

Here's an idea, converting the same example from above:

import {ExternalClass} from 'third-party-library'
import EventEmitter from 'events' // from Node.js

// simple syntax
class OurClass extends ExternalClass, EventEmitter {...}

const o = new OurClass
o.on('some-event', () => {...})

I'd also need to iron out syntax for calling specific constructors with specific args and calling specific methods when there are name collisions.

Maybe this can eventually turn into a proposal, but I'd first like to iron out the behavior, and I'd also like to implement it in native code (fork Firefox, if I get that far).

Few major questions here:

  1. How do you manage reflection? With the current system, you can do Object.getPrototypeOf(object) and get the prototype, but how do you do that with multiple inheritance?
  2. How do you set up that inheritance chain with plain objects? JS internally is entirely prototype-based and classes can be entirely desugared to prototypes + new.target + weak maps for private fields, and multiple inheritance has to interop with that somehow.
  3. What would Object.getPrototypeOf(OurClass.prototype) return and what would Object.setPrototypeOf(OurClass.prototype, foo) do?

Please see https://www.npmjs.com/package/smart-mix
It works with classes and without classes. Works with public and private data (that can be shared between mixins). See the ' Advantages over other mixin approaches' section in the README.

1 Like

I didn't plan on doing that. With the current API, we just make instances, read or write properties, and call methods. The end use case is not for meta programming, but for regular scenarios (we new something, then use the APIs, or we extends from the classes).

However, I think it would be possible to monkey patch Object.getPrototypeOf to return a Proxy that can do the same branched property lookup, similar to what the multiple() implementation does internally.

I wasn't planning on doing that either. But maybe we can we can invent something like Object.setPrototypes() which accepts an array of objects, and internally create a Proxy to do the branched property lookup.

Maybe with such a new feature as multiple inheritance, we don't have to allow certain things to be doable. But if we did, we'd have to imagine some new APIs, like

Object.getPrototypesOf(obj) // return an array which are the children in the tree of prototypes for the current prototype.

We would just need to think of it as a tree structure, while staying backwards-compatible with prototype chains.

I do separately want to throw out there that multiple mutable prototypes are a massive PITA to optimize for thanks to all the complicated dependency tracking. You'd have to turn single-item fields into full arrays, and you'd have to only conditionally do this. Also, if you modify a prototype, it now doesn't just follow one tree, but several trees, and it also has to detect if other trees have that property during that tree walk (as to not modify overridden methods). And obviously, cycle detection would go from walking a list to a full tree traversal, something I don't see a lot of engines being happy with. The biggest concerns beyond how to retrofit it into the language are around performance, not semantics (which have been well-studied for years, in large part due to C++ and Python).

Also, keep in mind things like mixins do fulfill 90% of the use case of multiple inheritance, and implementing that only takes at most 10% of the effort.

class Foo extends One, Two, Three {
  // ...
}

would be so nice!

Maybe instead of relying on prototypes (a tree of them) for the whole class definition, the above could behave as

class Foo extends One { ... }

while for any properties that are not found on this the lookup could simply delegate to instances of Two and Three.

this would be instance of Foo, and the other two instances of Two and Three would not directly be directly referenceable by the end user, and accessing props on this (instance of Foo, but delegating to other instances) would be the only way to access props.

If we did it this way, I think it would mean that engines could optimize each instance of a class the same way they already do, and they'd have a hierarchy (tree) of lookup tables for props/methods.

Wdyt?

I'd like to add that, in theory, with the above described approach, it is possible that all optimizations an engine could do on classes written in a fully static optimizable way for single-instance instances would carry over to multi-instance instances. Each instance in the hierarchy would have individually optimized classes.

If all instances in the hierarchy have essentially a static lookup table (when code does not do anything deoptimizing like modifying prototypes or adding/removing descriptors, etc), then the worst case would be that the engine would have to lookup on multiple tables, one per instance (one per class), but the best case is that the engine could combine tables into one.

This is theoretical of course, and I think there's still much optimization work to do even on our current single-inheritance classes from what I can tell.