Composition and inheritance: the Julian way

That’s the trickiest thing; actually I’d say that it’s generally impossible, but not for lack of tools, but because by definition there is no closed list of “methods for X” or “methods for Foo”.

methodswith(X) may help you get a list of methods that you would like to extend at some point, and with a bit of metaprogramming you might extend all of them at once so that they work for Foo instead of X.(*) But then, maybe you do using Bar, which defines new methods for X, and your “extension” becomes incomplete.

And the “bad news” is that there is no way at all to anticipate what new methods may come from other packages. (Of course “bad news” only for the purpose of what we are discussing; in general that’s what makes composability easier.)

(*) Even for a limited list of “methods for X”, extending them so that they take Foo instead of X is not always straightforward. For instance, how would you extend *(::X, ::X)? Shall it be *(::Foo, ::Foo), or also other combinations with X and Foo?

4 Likes

Exactly. So Julia itself must have some rudimentary support of concrete single inheritance. Is there any technical difficulty in adding this?

No, there is no structural inheritance. That’s what this whole thread is about. Having structural inheritance makes inlining much harder, forces the compiler to chase endless pointer chains by default and makes the size of an given struct possibly indeterminate (possibly preventing SIMD).

The point is that choosing composition over structural inheritance makes that choice explicit and makes the job of the compiler easier. You can’t subtype concrete types in julia and abstract types don’t have fields. Together with functions and their arguments not being coupled to any particular struct definition, you’re free to define methods extending a given implementation with your type without having to touch the existing code at all.

The functions (i.e. operations) define your API - not necessarily your struct layout, i.e. how you store what you need.

6 Likes

Couldn’t you use a macro to do something like


@heritable struct MyParentType 
    my_field::SomeOtherType 
end

Which writes the code:

abstract type MyParentType end 
struct ConcreteMyParentType
   my_field::SomeOtherType 
end

and then write

@inherit struct MyChildType <: MyParentType 
    extra_field::SomeType
end

which writes the code:

struct MyChildType <: MyParentType
   parent_type::MyParentType 
   extra_field::SomeType
end 
MyChildType(x1::SomeOtherType, x2::SomeType) = new(MyParentType(x1), x2)
function getproperty(m::MyChildType, s::Symbol) 
    if s .∈ fieldnames(m)
        return getfield(m,s)
    else 
        return getfield(m.parent_type,s)
end

All methods defined on MyParentType are actually defined on an abstract type because the the macro defines the struct with the “concrete” prefix. Methods defined on MyParentType will correctly work on a subtype of MyParentType because the getfield function also looks for the fields in the parent type – unless the child type is given a field with the same name as a parent field.

I’m not sure if it would be possible to conveniently extend this approach to multiple inheritance, because you would have to write a macro extending methods defined on the second parent type to MyChildType, which is not a proper subtype of the other parent.

1 Like

People have written things like that. But, no one uses them. (and the author of the package below never intended for it to be used.)

https://github.com/tbreloff/ConcreteAbstractions.jl

The problem with these approaches is that they necessitate changes upstream.

Surprised that the following JuliaLang/julia Issue hasn’t been discussed here yet: https://github.com/JuliaLang/julia/issues/4935. The proposed feature exactly addresses the problem debated here.

1 Like

Just for reference for people reading this thread I figure I’d highlight [ANN] CompositeStructs.jl which is maybe the least restrictive of these packages (ConcreteAbstractions.jl, Mixers.jl, Classes.jl, …) that try to mimick inheritance. Anyway, like the author of ConcreteAbstractions, I don’t feel strongly that this is a “great” solution that should be used alot, but maybe useful in certain situations.

3 Likes

This thread is gold. Got to this point and now I understand a lot more about OOP and Julia type dispatch style paradigms (which I like better, now I know why). FWIW I’m not a CS person.

2 Likes

Hi dfdx,

your idea seems to solve my requirements exactly. I have to define a complex state that sometimes contains substates of partially the same entry types as the main state itself.

I wonder how to do this inheritance using Base.@kwdef. If I modify

res = :(mutable struct $name end)

into

res = :(Base.@kwdef mutable struct $name end)

I get the error: syntax: invalid type signature
Seems it’s a matter of macro evaluation…

Thanks

Torsten

I didn’t really read this thread or the first post, just thought this new package might be of interest:

2 Likes

If ObjectOriented.jl doesn’t solve you problem, you can check what exactly is generated by the macro using @macroexpand <expr>.

Wait a sec - I don’t see how multiple dispatch has much of anything to do with the composition/inheritance/reuse discussion happening in this discussion.

I’m totally on board with multiple dispatch and I don’t think it’s very hard to understand. But I’m desperately missing some kind of inheritance, by that name or any other.

I’m going to go read Type-Dispatch Design: Post Object-Oriented Programming for Julia - Stochastic Lifestyle and see if that helps me.

I was being a bit blunt, but multiple dispatch really has a lot to do with inheritance, because its the mechanism for defining traits.

I guess its not really clear what you mean by inheritance - fields or behaviors? But once you get used to it you wont miss anything, you are on a familiar arc.

I’m totally on board with multiple dispatch and I don’t think it’s very hard to understand.

I guess there is more to it than a basic understanding. Have you written your own “holy traits”?

But heres an example of how multiple dispatch interacts with inheritance of fields and behaviors using composition. Object has some fields and behaviours of its own, and behaviors and even fields from Style objects. These influence the results of some_function using multiple dispatch.

abstract type Style end
struct StyleA <: Style end
struct StyleB <: Style
    val::Int
end

struct Object{S}
    style::S
    val::Bool
end

some_function(o::Object) = some_function(o.style, o)
some_function(::StyleA, o::Object) = o.val
some_function(t::StyleB, o) = t.val > 0

This function could be defined as a fallback for a new StyleC without any changes to Object

some_function(c::StyleC, o) = c.val^2 > 10

And by using StyleC() as your style in Object will give you StyleC behaviours. But you could additionally write a method to dispatch on both and handle the interaction.

(if youre still wondering why, now think about adding another composed object and dispatch for combinations of it and Style, and how you would just add another argument to some_function and define a few methods, and it will work and extend to whatever complexity of inheritance you need…)

10 Likes