Composition and inheritance: the Julian way

The problem with this is that it is inherently type unstable

Oh man. How terrible is that? Is there a way to fix it?

Thanks

It looks like no matter what I do, I am going to get a Union type for the output of nationality. Is that so bad? I thought Unions were fast now :pray: I’ll need to rewrite a whole lot of code is this is a no no :pensive:

Depending on what you do, it might matter or it might not.

In this case, I don’t see the advantage over a

function nationality(a::Person)
    isnothing(a.nationality) || return a.nationality
    error("Call the sheriff!")
end
3 Likes

Thank you for pointing this out. I went ahead and modified my code and @code_warntype is all blue now :smiley:

For my case, it was probably ok. I am competing against Excel, so the bar is pretty low, but still :blush:

I feel better now though so thanks again :raised_hands:

I think you should avoid trying to match traditional OOP too much. See if there is a more Julia friendly way of solving the same problem.

But of course there are problems where OOP feels natural. So here are some of my tips. I got one approach used in a package called Figurer, which specifically tries to model OOP style hierarchies for geometric shapes. You got a Square is a Rectangle, which is a Parallelogram, which is a Quadrilateral.

I have basically created the whole inheritance hierarchy in terms of abstract types. Every abstract type which needs it had a corresponding concrete type with the actual data. I try as much as possible to define functions operating on abstract types and only revert to the concrete types when needed.

So essentially you got a tree structure where data only exists at the leaf nodes.

That is one approach. It is very useful in this case because “subclasses” don’t share data. E.g. a Square is a subclass of Rectangle, but a Square should not have width and height fields, only a side.

In other cases I use the template method design pattern, which is good form anyway. One of the flaws of traditional OOP is that you NEVER know if you should call the super method and if you do, when? In the beginning or the end. Perhaps in the middle?

Let me use your code example to demonstrate usage of template methods:

function call(p::AbstractPerson)
    if !do_call(p)
       print_with_color(:red, uppercase(p.name), "!")
    end
end

do_call(p::Person) = false

function do_call(p::AbstractCitizen)
    if p.nationality == "Italian"
        print_with_color(:red, uppercase(p.name), " dove sei ?")
    elseif p.nationality == "UK"
        print_with_color(:red, uppercase(p.name), " where are you ?")
    else
         return false        
    end
    true
end

There are lots of ways of dealing with types and shared functionality in Julia. Another approach you can also see in my Figurer example is how I’ve done the Point, Vector2D and Direction types. These are all representing an x, y coordinate pair in different ways. So there is a lot of shared functionality, but they are not the same thing. There should not be an inheritance hierarchy. In this case I utilize type unions as argument to implement the same function for multiple types.

4 Likes

@ScottPJones, thanks for suggesting this pattern – I think it works well in many cases. Is there a general name for this pattern? It seems like a simple workaround for allowing concrete inheritance using only base Julia.

Helpful piece of code. I added <: supertype($base_type) now the Citizen mutable struct is also a sub type of AbstractPerson

I read almost all of this thread, and I learned quite a bit. But I still fail to see this one important concern addressed:

  • extend X to obtain a new object called Foo;
  • use Foo in exactly the same way I would use X object;
  • re-defining ONLY those methods for which the Foo behaviour differs from the X one, which in the actual case are 3 or 4 (while the X object is accepted by hundred of method);
  • adding some metadata fields to Foo
  • do it by myself without asking the Xs maintainers to change something in their package.

I completely agree that the base package X could be written in such a way so as to allow easy extension/compositon, but a language should not assume perfection in third-party code, and indeed, perfect design might simply be overoptimizing in a lot of cases, and not worth the engineering effort.

2 Likes

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?

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

5 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:

1 Like

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