As per title, can I impose my
abstract type MyAbstractType end
as a/the supertype for a pre-existing struct/type ExistingStruct
coming from, say, a separate module? I just want
ExistingStruct <: MyAbstractType
to return true
.
As per title, can I impose my
abstract type MyAbstractType end
as a/the supertype for a pre-existing struct/type ExistingStruct
coming from, say, a separate module? I just want
ExistingStruct <: MyAbstractType
to return true
.
No. Can you describe the situation in which this is needed, so we can suggest alternatives?
I have an abstract type InitialValueProblem
and a function (a solver) that takes any concrete subtype of InitialValueProblem
and solves it. Such a solver can be used with OrdinaryDiffEq.ODEProblem
too, so I wanted to give the user the possibility to make OrdinaryDiffEq.ODEProblem
a subtype of InitialValueProblem
and use the solver.
you might want to use a function to check a property for example
hasthisproperty(_) = false
hasthisproperty(::ExistingStruct) = true
# or
hasthisproperty(::Type{ExistingStruct}) = true
It seems to me that you have in your mind the limitation that your function/solver can only take concrete subtypes of InitialValueProblem
. Why do not define (or just let the user define) a new method of the same function/solver which takes an OrdinaryDiffEq.ODEProblem
instead of a subtype of InitialValueProblem
? Functions are extensible.
Another option is the user defining a new struct which is a subtype of InitialValueProblem
and has a OrdinaryDiffEq.ODEProblem
inside it, and extends for this new struct any of the functions you expect to work on a InitialValueProblem
subtype.
As an example of my first suggestion, you can write:
module Original
import OrdinaryDiffEq
function solver(x :: InitialValueProblem)
...
end
function solver(x :: OrdinaryDiffEq.ODEProblem)
...
end
export solver
end
Or you can leave to your user to do so:
module UserModule
import Original, OrdinaryDiffEq
function Original.solver(x :: OrdinaryDiffEq.ODEProblem)
...
end
end
The problem with this approach in my case is that the solver calls a plethora of other functions which all take InitialValueProblem
as input.
I think the second option you’ve suggested is the best way to proceed for me.
My question then becomes: “why these internal functions expect a InitialValueProblem
?” At some point, I believe, you must call methods for the specific subtype, not for the generic InitialValueProblem
abstract supertype (otherwise the code would never interact with the instance of the concrete struct
). Seems to me that would probably be best to drop the :: InitialValueProblem
from the generic code and document which functions are expected to work on the parameters that had the :: InitialValueProblem
restriction before, so any users know what to extend for their struct
or a third-party struct
to work with your solver
function.
Indeed, this is what I had done before even asking the question, but I was hoping for ExistingStruct <: MyAbstractType
(or any other workaround) to actually work. This is because I LOVE the inheritance structure. I do realise, however, that removing ::InitialValueProblem
might have actually been the simplest, and thus best, solution.
I probably should follow my father’s advice that “color, taste, and love are not something you should make arguments about”, but I would like to know why you love the inheritance structure. I am on the other side of this camp, and I really would like to have some insight on the positive sides.
Unfortunately, my father did not give me the same advice, so I’m stuck with coding based on personal taste
On a serious note, I believe that inheritance gives you the possibility to add “flavour” to your variables. For example, let’s say that you are an applied mathematician and that you have a struct called IncompressibleNavierStokesProblem
. The name might seem quite descriptive, but it does not give you any idea of what kind of solvers apply to it, etc. Setting it to be a concrete instance of, say, MortarBasedFiniteElementProblem
can immediately give you an intuition of how IncompressibleNavierStokesProblem
is actually treated and solved. And you can extend this argument for how many nested types you want. A similar argument applies to functions (say, a function solver
with a method specifically working on MortarBasedFiniteElementProblem
s). In my opinion, this helps a lot connecting theory with actual code implementation, especially if you are reading someone else’s code implementation.
But this is just what works for me.
Inheritance does provide additional hints about how a struct
may be classified no doubt, but in Julia and many others languages (of different reasons), a class/struct
cannot inherit from two or more supertypes. So if someone creates a new way to solve your IncompressibleNavierStokesProblem
but its is already a subtype of MortarBasedFiniteElementProblem
then there is no way to use the inheritance mechanism for the new way of solving. You could go for the route of allowing for multiple inheritance, but this route has its own problems.
Consequently, my ideal goes against the the use of inheritance at all, even in Julia. Ideally I would organize my code the following way: I create a module for some concept I want to be able to work with, let us say, Numbers
, and so I export a lot of functions without no method associated (i.e., no implementation whatsoever). If I created an algorithm that works over Numbers
, then I import that module and call functions defined on it inside my functions over parameters I do not specify the type. If I want to create a new kind of number, I import Numbers
and create a new struct for which all methods exported by Numbers
are defined for it. Then any new Numbers
type work with any algorithm for Numbers
even if both were developed in separate. Also, you can easily create, or adapt from third-party, a struct
that is both a String
and a Number
or any number of concepts you want simultaneously. If someone creates a different concept of what Number
should look like, then anybody can wrap any previous struct
to this new concept using the old concept (or relying in the internals of the struct).
Why would that be counterproductive? It seems to me that, at the end of the day, multiple inheritance is almost like having no inheritance at all, except that you have the advantage of attaching additional descriptors, as I was mentioning above. I’m no CS savvy, so I simply don’t know.
Well, it depends on the language. The problem often boils to the diamond
inheritance, this is you have a class SuperSuper
, and its has two subtypes Super1
and Super2
, and then for some reason, you want to create a Child
type which inherits both Super1
and Super2
(it is called diamond
because the shape of this in a diagram remebers the shape of the respective card suit). In C++, for example, the Child
would have two copies of every field in SuperSuper
coming from two distinct parents (or you could thing of combining the fields back, but this means Super1
methods would interfere in the same state Super2
methods interact). In Julia, Jeff would probably go crazy trying to decide which method signatures are more specific (what is already hard enough as now, when the type hierarchy may be represented by a tree not a DAG). I am no expert on the subject, and probably some study will reveal many other underlying problems, but the fact is that my impression from many successful languages is that they found multiple inheritance to not be worth the headache.
However, one should not confuse Multiple Inheritance
with Interfaces
with is a system similar to the one I proposed (but not exactly the same), in which the Type
extends an Interface
(as a Type
inherits another Type
), and is probably closer to your impression of “is almost like having no inheritance at all”.
Thank you for the detailed reply, as well as your overall input; I’ll make sure to dig in deeper about this topic.
Just a technical note: Julia does not have what most languages describe as inheritance (= aquiring slots and properties from parent) at all, just subtyping.
AFAIK the motivation for Julia’s type system design was a a trade-off between expressiveness and complexity. The subtyping problem as is (with UnionAll
types, etc) is already pretty involved, but is very expressive too.
Finally, you can always use traits for problems where subtyping does not suffice, they are also much more powerful.