Can I impose a super type for an existing struct?

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?

4 Likes

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.

2 Likes

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
2 Likes

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.

5 Likes

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.

1 Like

Unfortunately, my father did not give me the same advice, so I’m stuck with coding based on personal taste :smile:

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 MortarBasedFiniteElementProblems). 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).

3 Likes

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.

1 Like

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”.

1 Like

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.

3 Likes