Open avahe-kellenberger opened 4 years ago
We removed multiple dispatch recently to make the compiler simpler and the code generated faster https://github.com/nim-lang/RFCs/issues/65
Multiple inheritance seems to be introducing complexity and also make the generate code slower as well due to having to introduce branches in the dispatching code (single or multiple inheritance then checking the class methods).
Furthermore, just like multi-dispatch can be implemented in a library with a visitor pattern for a single-dispatch language (not that I like it) with decent performance (all C++ compilers including Clang uses this pattern), you can use macro today to implement multiple inheritance on top of object variants or concepts or methods.
For example here is how you can emulate classes with variant types: https://github.com/mratsim/trace-of-radiance/blob/99f7d85d/trace_of_radiance/support/emulate_classes_with_ADTs.nim#L246-L268
type
Color = object
Ray = object
HitRecord = object
Metal = object
Lambertian = object
Dielectric = object
func scatter(self: Metal, r_in: Ray, rec: HitRecord): Option[tuple[attenuation: Color, scattered: Ray]] =
debugEcho "Scatter on Metal"
func scatter(self: Lambertian, r_in: Ray, rec: HitRecord): Option[tuple[attenuation: Color, scattered: Ray]] =
debugEcho "Scatter on Lambertian"
func scatter(self: Dielectric, r_in: Ray, rec: HitRecord): Option[tuple[attenuation: Color, scattered: Ray]] =
debugEcho "Scatter on Dielectric"
declareClass(Material)
registerSubType(Material, Metal)
registerSubType(Material, Lambertian)
registerSubType(Material, Dielectric)
registerRoutine(Material):
func scatter(self: Material, r_in: Ray, rec: HitRecord): Option[tuple[attenuation: Color, scattered: Ray]]
You can easily go the over way around, create an abstract scatter type (or generic, or variant or inheritable) and then define your proc on top and via macro "register"/inherit them to other types. This would be significantly faster, especially for gamedev, because it completely avoids GC, uses static dispatch and preserve memory locality instead of dereferencing random heap location and incurring cache misses.
Now beyond what is possible today, instead of inheritance I'd rather have concepts be expanded to work on runtime types https://github.com/nim-lang/RFCs/issues/168 and encompass Rust traits and Go interfaces in terms of capability.
Now beyond what is possible today, instead of inheritance I'd rather have concepts be expanded to work on runtime types #168 and encompass Rust traits and Go interfaces in terms of capability.
Interfaces are okay, but having real multiple inheritance would be less restrictive.
For the implementation, if we can use something better than multiple dispatch I'm all for it. My main goal is to enable the programmer to have the functionality/syntax as I've described in the post.
I like that approach, was a good read.
I think the main issue was described in my post, how to know what functions are called when the diamond exists. To reiterate:
Missile inherits Projectile
and ParticleEmitter
, which both inherit Updatable
. These all have a common function, update
.
method update*(this: Missile, deltaTime: float) =
echo "I update particle emitters"
procCall Updatable(this).update(deltaTime)
echo "I update projectiles"
This is the implementation of Missile's update
function, when the user does not override the behavior. The procCall is a shared line of code to Updatable
from both Projectile
and ParticleEmitter
. The args passed into the super class' function are unchanged, which is the most common use case - we can search for duplicate invocation in the diamond, like in this example, and reduce it to a single call.
The reason the first echo statement is before procCall
, is the implementation of ParticleEmitter
's update. This allows the programmer to order code as they see fit, and we maintain the order during compilation.
This was all stated previosly but not explicitly, just wanted to make sure I got the point across. If more explanation is needed, let me know. I could attempt drawing diagrams, write more examples, etc.
What is needed is a prototype implementation via Nim's macro system and then we can see if or how macros are less than ideal.
What is needed is a prototype implementation via Nim's macro system and then we can see if or how macros are less than ideal.
I think a natural implementation with macros would be to automate the generation of type implementations given in the of
list:
multitype Child of Mom, Dad:
...
method method_1(c:Child)=...
.
.
.
method method_n+m(c:Child)=...
==>
type Child = object
var em:ExtendedMom
var ed:ExtendedDad
....
method method_1(em:ExtendedMom,...)=...
.
.
.
method method_n(em:ExtendedMom,...)=...
method method_1(ed:ExtendedDad,...)=...
.
.
.
method method_m(ed:ExtendedDad,...)=...
But then, you are still required to explicitly extract the appropriate object out of Child
when passing to a function expecting Mom
or Dad
which is the annoying part.
Alternative solutions to allowing multiple inheritance would be to allow for implicit conversions or to overload the :
operator in general, which would be the same thing, I think.
Another problem is to retrieve all methods of a type which lie somewhere in the current module in order to change the signatures to the generated extended types. I think we need some kind of reflection for this, don't know how to solve for this otherwise.
See also: https://dev.to/xflywind/zero-overhead-interface-exploration-in-nim-language-2b3d
Multiple inheritance seems to be introducing complexity and also make the generate code slower as well due to having to introduce branches in the dispatching code (single or multiple inheritance then checking the class methods).
The question is if we could find a mixed approach to omit branching when multiple parents aren't available, i.e. we only use single inheritance here.
Dart has single inheritance but supports mixins. I'm no programming language designer, but it is my understanding that mixins are easier to implement than multiple inheritance, while preserving key benefits. As such it should also be more performant than multiple inheritance is my guess.
What are your thoughts ? has this already been considered? Is this an avenue worth exploring ? Could mixins be a suitable alternative to multiple inheritance for Nim without some of the drawbacks?
Mixins can probably be elegantly implemented as a library if https://github.com/nim-lang/Nim/issues/13830 is fixed, combined with concepts maybe. Fixing that issue allows "mixing-in" procs and things outside of an nnkTypedef
. There's also https://github.com/nim-lang/Nim/issues/13830#issuecomment-609031271.
What is Multiple Inheritance (MI)
MI allows an object/class to inherit the characteristics and features of more than one parent object or class.
From this point forward I am going to refer to object types as
classes
for simplicity.Why would we want it?
I will start by saying, MI is not always the right tool for the job - but sometimes, it is (or at least is a decent option).
Multiple inheritance allows for easy composition of small classes that implement independent functionality and can manage their own state. When used correctly, we end up with a lot of small and reusable code without having to duplicate functionality or data structures.
Why don't you just use ECS?
I've yet to see an ECS implementation that wasn't tedious and difficult to deal with.
MI reads and writes cleanly.
Example of MI in Nim
This is how I envision multiple inheritance in Nim:
In this example, Player inherits the
x
andy
properties ofLocatable
, and we could now invoke the methodupdate
with aPlayer
as a parameter.Locatable and Updatable are free to be reused by any new objects, without having to affect the structure of
Player
.Deadly Diamond of Death!
Most complaints regarding the implementation of MI, is The Diamond Problem.
Here is an example:
Let's create the problem with some Nim pseudo code to illustrate:
The Diamond:
If we invoke
missile.update(0.15)
as shown above, we don't know which update should be invoked. Should it callProjectile
's update, orMissile
's update? In this case, a complier error should be thrown due to ambiguity.Solutions
There are many ways other languages have solved the diamond problem (see this wiki article about mitigation)
My proposed solution is to force the user to implement the method, and use procCall to invoke any parent implementations they want.
From the above example, the user could write this for the
Missile
:In this case, we could detect calls to the base class (Updatable) and structure our code to prevent multiple invocations. The code would be translated as:
Closing Thoughts
I've seen multiple people wanting some sort of multiple inheritance in Nim (see this post as a recent example) - I think it would be a good change that would not negatively affect those who don't care for the object oriented paradigm.
@Clyybber