Composition and inheritance: the Julian way

Don’t count me in your list I just want slightly more code reuse which has nothing to do with OOP

I know, it’s somehow related but you’re right, it’s not the same problem.

I’m sure most of the competing solutions you mention will die in the next few years anyway.

Probably my question came too early and after a few years, a unique solution will be considered as standard.

Just learn multiple dispatch.

Let’s say I come from an OOP language and I have no idea what multiple dispatch is. So far, there will be dozens of way of writing pseudo-OOP that will be rewrited in multiple dispatch. Some are more efficient than others, some more readable and some less efficient and less readable. New julians will continue to use theses tools and write a dozen more, no matter what you might think of it.

People create experimental libraries to explore various ideas. Some of these pan out, some don’t and just wither away.

Some new users will inevitable discover these and experiment for a while, and there is nothing wrong with this. It takes years to learn a new language well, and exploring dead ends is part of the learning process.

2 Likes

Some new users will inevitable discover these and experiment for a while, and there is nothing wrong with this. It takes years to learn a new language well, and exploring dead ends is part of the learning process.

If the dead end is more readable and take less code to write, why would it be a dead end ?

I am not sure what you are talking about here — please be specific.

I will pick that package for the sake of the example, but it’s the same story with another.

I prefere writing this code

using ConcreteAbstractions

@base type AbstractFoo{T}
    a
    b::Int
    c::T
    d::Vector{T}
end

@extend type Foo <: AbstractFoo
    e::T
end

@extend type AnotherFoo <: AbstractFoo
    z::String
end

rather than that one:

abstract AbstractFoo
ConcreteAbstractions._base_types[:AbstractFoo] = ([:T], :(begin; a; b::Int; c::T; d::Vector{T}; end))

type Foo{T} <: AbstractFoo
    a
    b::Int
    c::T
    d::Vector{T}
    e::T
end

type AnotherFoo{T} <: AbstractFoo
    a
    b::Int
    c::T
    d::Vector{T}
    z::String
end

It’s the same after compile time. But it’s more readable and less prone to errors.

If field inheritance is what you want then, sure, the former is nicer. That’s why ConcreteAbstractions exists and there is nothing wrong with using it.

However, maybe you don’t actually want field inheritance in the first place. Maybe you’re somewhat biased by your OOP background. I recommend you take a look at this great, although old, blog post:

I found it to be very instructive!

4 Likes

This might not be a great solution, but I’ve found myself doing things like this…

In the original post, I would define a big Person type that has anything you’d want to know about a person, e.g.

struct Person
    name::String
    age::Int
    nationality::Union{String,Nothing} # You should be able to add this without breaking any existing `Person` methods
end

then I would use Holy traits (as I understand them), e.g. define singleton types with a method to insert a type for later dispatch, e.g.

struct Citizen end
struct Outlaw end

persontype(a::Person) = isnothing(a.nationality) ? Outlaw() : Citizen()

nationality(a::Person) = nationality(a,persontype(a))
nationality(a::Person,::Outlaw) = error("Call the sheriff!")
nationality(a::Person,::Citizen) = a.nationality

Not an abstract type in sight. Using this trick makes me think of Person as a “abstract type with fields”.

Similarly, in the more recent example, I would define an encompassing AbstractFoo type

struct AbtractFoo{T}
    a
    b::Int
    c::T
    d::Vector{T}
    e::Vector{T}
    z::Union{String,Nothing}
end

struct Foo{T} end
struct AnotherFoo{T} end

footype(f::AbstractFoo{T}) where T = isnothing(f.z) ? AnotherFoo{T}() : Foo{T}()

dostuff(f::AbstractFoo) = dostuff(f,footype(f))
dostuff(f::AbstractFoo,::Foo) = # Do Foo stuff
dostuff(f::AbstractFoo,::AnotherFoo) = # Do AnotherFoo stuff

Edit: There is one more thing I do. I’d add constructor-looking methods:

Outlaw(name::String,age::Int) = Person(name,age,nothing)
Citizen(name::String,age::Int,nationality::String) = Person(name,age,nationality)

Similarly

Foo(a,b,c,d,e) = AbstractFoo(a,b,c,d,e,nothing)
AnotherFoo(a,b,c,d,e,z) = AbstractFoo(a,b,c,d,e,z)
4 Likes

OK, so I can reformulate my very first post because it wasn’t clear apparently.

My main problem is that there is no standard way to do field inheriance.
I’ve used ConcreteAbstractions but I could use StructuralInheritance or almost every packages of the list.

Thank you for the link, I will read it.

Most Julia code purposefully avoids field inheritance, and uses composition combined with forwarding minimal core interfaces (a FAQ is “how can I forward all methods” — you can’t and you shouldn’t; use/define an interface, even informally).

This meshes well with having no explicit annotation for “private” and “public” fields, as implicitly all fields are “private” in the sense that they are part of the implementation and it is not a good idea to access (let alone modify) them unless this is explicitly documented as supported by the API. In the ideal case, a caller outside the module should not care what the fields are. They are implementation details and could change at any point.

Allowing the extension of Base.getproperty & friends in v1.x changes this picture a bit, as this introduces the possibility of an API that relies on syntax that is superficially like field access. I think that developing habits for this is WIP for most people, as the feature is very novel.

This is less documented than it should be, except for blog posts like the above. So I fully understand that people coming from C++ and similar OOP languages with baroque multiple inheritance rules feel a bit lost initially. I think the best solution to overcome this is to read a lot of code in Base, or packages from experienced Julia developers like Tim Holy, and try to experiment with various idioms. You may find them to be quite different from C++, but very powerful at the same time.

4 Likes

& Functional Programming.

The reason OOP is so popular is that it’s not just a programming technique, it’s a domain modelling system. Multiple dispatch is more of a programming technique, it doesn’t offer the same direct mapping from problem domain to code.

I find I need to use Julia in a more functional programming way to effectively model complex domains in code. In that case you are modelling the flow of data in a kind of pipeline vs mutation of stateful objects. Multiple dispatch indeed is very effective here.

3 Likes

@atthom

As a more concrete step in transitioning from an OOP language to Julia try implementing the iterators interface for your types ( where it make sense ). Interfaces · The Julia Language

1 Like

The problem with this is that it is inherently type unstable, e.g.

> @code_warntype nationality(Person("Tim", 5, nothing))
Body::Union{Nothing, String}

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