Hi, I have defined an abstract type AbstractQuantumParticle, and then a specific structure QuantumParticle <: AbstractQuantumParticle
which has some fields like position and momentum.
Let us say now I want to add an extra particle Fermion <: AbstractQuantumParticle, which also has a spin variable, but that also works with all the methods that interface with QuantumParticle via the multiple dispatch (plus some specific dispatching for it).
The easiest solution is to copy all the content of QuantumParticle inside Fermion, and add the spin. However, in this way if I have to add something to QuantumParticle in the future, this needs to be done also to the Fermion structure.
Is there a more elegant way (via a decorator, for example) to tell the Fermion structure to track and copy all the attributes of the QuantumParticle structure?
This is similar to OOP inheritance but much simpler (I do not want to go OOP, no class, just a simple structure). I saw many packages to bring the OOP paradigm in Julia, but that is not what I want; a simple tracker that acts like a precompile directive would be much better.
There isn’t a straightforward way to do this because it’s exactly what inheritance is, and concrete subtyping does not work well with multiple dispatch. This is a reasonable want, though, and something similar is done.
Perhaps you’ve heard of the composite reuse principle of OOP, or “composition over inheritance.” Julia does it much more by necessity. If you want all QuantumParticle methods to work on Fermion, the easiest way is to have Fermion contain QuantumParticle, then you just forward foo(fermion.quantumparticle). You could dispatch a getter like getquantumparticle over different types so you aren’t writing and branching to various dot notation sequences everywhere. That getter can be incorporated into a foo method as well for a short foo(fermion). (I’m assuming this is a toy example, not an actual use case where you would likely prefer to have actual fermions as concrete subtypes of an abstract Fermion). Refactoring overuse of inheritance and dot notation is a very OOP practice, so this isn’t even a culture shock.
Different packages like MacroTools.jl and Lazy.jl have made forwarding macros to pull off something closer to how concrete inheritance feels, but I’m not aware of a standard package maintained for this purpose. Strictly speaking, forwarding is not ever inheritance because inherited methods work directly on an instance of the subclass, not the superclass.
Sure, you can do this, but the exact implementation depends on what the API is for QuantumParticle.
Eg if you expose get_position and get_momentum, you can just implement these methods to forward the value from the relevant field for Fermion. If you want the Base.getproperty to work (.position), you need a different approach, but it is doable.
Please provide specific and then you may get a more detailed answer.
You really don’t want field inheritance. I wrote Mixers.jl to do this years ago and never use it. Most similar packages are also not widely used. While field inheritance at first appears to simplify things, you will find it gives you much less flexibility at the cost of very few lines of boilerplate, in a language that is otherwise already very succinct.
For performance and simplicity, I recommend you just define getter methods rather than using getproperty. Put them on the abstract type pointing to default field names
And if you e.g. don’t need some field for a subtype, just define the same method on the subtype to return a default. You cant do that with field inheritance.
This way the only boilerplate is the struct fields.
If you have too many fields, compose your objects from smaller components that can be shared between similar objects. (This is call composition over inheritance and is widely recommended practice in OOP anyway. Julia just bakes it in because there is no field inheritance. Composition over inheritance - Wikipedia Also with composition, macros like Lazy.jl @forward may confuse people reading your code, I personally just write out the forwarding methods)
You are right; probably the best approach is to store the QuantumParticle inside the Fermion to avoid redefining its components. But then, for the function foo(particle :: QuantumParticle) to work with foo(fermion) as expected, I need to dispatch it like foo(fermion <: Fermion) = foo(fermion.quantumparticle).
Indeed, combining this with getter methods might avoid dispatching all the functions implemented with QuantumParticle. So, I only need to dispatch the getter methods.
It is hard to provide more concrete advice without specifics, but you can do stuff like
gimme_the_generic_quantumparticle(fermion::Fermion) = fermion.quantumparticle
function foo(particle)
generic_qp = gimme_the_generic_quantumparticle(particle)
... # implementation here using the above
end
for methods that work with the generic part. You just define methods for gimme_the_generic_quantumparticle.
Thank you for stating this because I ran into Mixers a while ago and came to the same conclusion after a struggle, but I had a lingering suspicion I just didn’t understand how to use it. I could only accomplish method reuse for a set of fields by making an unenforceable, documented promise that an abstract type’s subtypes all share those fields, which was an abandoned idea (abstract types with fields · Issue #4935).