Property Inheritance Pattern

What do you mean by type ineritence? Methods in Julia can be written to accept subtypes of a type, so I’m kind of confused by what problem this is designed to solve.

I guess mainly the problem of property inheritance. Sub types do not inherit the properties of their super types. A naive approach to solving this problem would lead to massive amounts of code duplication. For example in an object oriented package im currently porting, the base class maintains allot of internal state. None of which needs to be seen by sub-classes. If all the sub-classes had to carry this over it would become very cumbersome. However, the method i purpose is also cumbersome, but could be simplified with a macro.

It sounds like a simple approach would be to create a struct InternalState which all subtypes have as a field (if they want).

2 Likes

That is a possible solution. It would require each subtype have a copy of each of its supertype’s InternalState. And it would probably not be optional since methods from the base type will still need access to each of them. But i’m not sure how initialization logic would work in that case. Could the initializer of each internal state be encapsulated, such that you would not have to rewite the logic from the base initializer each time, especially of there is allot of complex initialization logic? But I do see that were dealing with edge cases and not the typical type inheritance scenario.

More than that: supertypes can’t even have any fields to inherit, because only concrete types can have fields, while only abstract types can be supertypes.

This makes your proposal a bit hard to read, since it appears confused on that point. But, as far as I understand, you’re not suggesting any changes to Julia, just showing a way of using composition to mimick inheritance?

As for

do you mean that an object of the supertypes maintains the state, or the class itself. Classes can hold state in Python, but not in Julia.

Can’t they all hold a reference to the same object, why copies?

1 Like

Im not suggesting any changes to Julia because libraries written from scratch can generally avoid this problem. But for me this is a problem when porting a library that has been written in a more traditional object oriented (OO) language and you don’t want to completely redesign everything. Even then i think @Oscar_Smith’s solution is fine most of the time.

Perhaps copies was the wrong word. But, depending how far down the inheritance tree a type is, subtypes would need to have one InternalState property defined for each super type.

abstract type AbstractFoo end

mutable struct FooInternal
   a
   # A lot more internal state properties
end

mutable struct Foo <: AbstractFoo
   foo_internal::FooInternal
end

abstract type AbstractBar <:  AbstractFoo end

mutable struct BarInternal <: AbstractBar
   b
   # A lot more internal state properties
end

mutable struct Bar
   foo_internal::FooInternal
   bar_internal::BarInternal
end

# a will always be accecable at .foo_internal.a without having to copy and paste 
# a lot of internal state properties for every subtype
say_something(foo::AbstractFoo) = println(foo.foo_internal.a)
say_something(bar::AbstractBar) = println("$(foo.foo_internal.a) $(bar.bar_internal.a)")

But my idea came from trying to make the type system a bit more OO like. So in traditional OO each object will have a super which will encapsulate the properties of the super type. On this forum people have suggested such a pattern for Julia:

mutable struct Foo
    a
end

mutable struct Bar
   b
   foo::Foo
end

mutable struct Baz
   c
   bar::Bar
end

get_a(foo::Foo) foo.a
get_a(bar::Bar) bar.foo.a
get_a(baz::Baz) baz.bar.foo.a

But the problem with that is when accessing a from Bar you have to use bar.foo.a This makes Bar incompatible with methods that call foo.a. So the naive solution for that is to have assessor methods.

get_a(bar::Bar) = bar.foo.a

Other than being annoying having to use get_a(bar) instead of bar.a this also creates the problem that you have to define all the accessor methods for Foo inside Bar and and do it agian if you want a Baz even if the property was only ever used by a method for Foo

My proposed pattern allows for a more traditional OO structure that makes for a more direct translation from OO languages. You can keep all the dot notation. Object initialization logic can remain intact without having to reuse initialization code in sub-types. And types stay mostly encapsulated. If you have a file that defines an object Foo with a property a that is never used in subclasses then it should be possible to write a second file for Bar that does not have to deal with a. And if later a is changed or removed, Bar is unaffected.

So you could do something like:

mutable struct Foo
   n
   large_array
   Foo(n) = new(n, zeros(n))
end

mutable struct NamedFoo
   foo::Foo
   name
   # we don't need to initialize large_array or worry about its implementation here
   NamedFoo(n, name) = new(Foo(n), name)
end

But this seems to be an example of multiple inheritance. That’s orthogonal to how far things are down the inheritance tree. Naturally, you N supertype-fields for N supertypes (or perhaps a tuple of supertype objects.)

Wouldn’t you just use

function getproperty(baz::Baz, sym)
    if sym == :a
        return baz.bar.a
    else
        ...
    end
end

and then Bar would have a similar definition, and it would cascade to the top level.

Or, even, more lazily

function getproperty(x::AbstractToplevelType, sym)
    if sym == :a
        if hasfield(x, sym)
            return x.a
        else
            return x.super.a
        end
    else
        ...
    end
end
1 Like

Basically this is exactly what im doing except differently. I guess performance wise I would have to try both. Mine uses multiple dispatch and yours uses if else statement. As far as simplicity yours is better.

This seems really different to me. You have to implement the full chain to the top for each type, instead of just recursively deferring to the supertype. Also, you use a special get_a instead of getproperty which you appeared to be dissatisfied with:

The way I’m suggesting, simply writing baz.a will work directly, no matter how many levels down you are.

get_a(baz::Baz) baz.bar.foo.a was my counter example, to show what was a bad idea.

See the code example in the initial post for the full example but what i do think might be a good idea is this:

abstract type FooSubtype <: AbstractFoo end

Base.getproperty(sub::FooSubtype, s::Symbol) = get(sub, Val(s))
# In the base-type, a fallback accessor is defined
get(sub::FooSubtype, ::Val{T}) where {T}  = getfield(sub, T)
get(sub::FooSubtype, ::Val{:a}) = sub.foo.a

Base.setproperty!(sub::FooSubtype, s::Symbol, x) = set!(sub, Val(s), x)
# In the base-type, a fallback accessor is defined
set!(sub::FooSubtype, ::Val{T}, x) where {T}  = setfield!(sub, T, x)
set!(sub::FooSubtype, ::Val{:a}, x) = sub.foo.a = x

This is basically the multiple dispatch version of what you suggested earlier

OK, I missed that. Should read your post over more carefully later.

But it seems annoying to have to use a special get function instead of getproperty. Performance for my version shouldn’t be any worse, I think.

The semantics of Julia favor composition over inheritance.

Search the forum for previous discussions, or the web for a broader context.

I found that pattern here Getproperty, decorations, inheritance in 0.7

I know there has been many threads discussing proposals to implement inheritance. But im just trying to find a method to simplify the porting of object oriented code.

I don’t think that a 1:1 port of code from an OO language is a reasonable goal. Julia works differently.

Coming up with an idiomatic organization of the code and the API usually has a lot of additional benefits.

3 Likes

While I agree with @Tamas_Papp that a straight porting from Python may not be optimal, you may take advantage of several packages providing Julia macros to simulate OOP patterns to a certain extent.

One such package is ReusePatterns, allowing both composition and concrete subtyping.

2 Likes

I think that using a lot of value types has fallen a bit out of favour since v1.0, as constant propagation has become more powerful. I don’t think there are any advantages to using them instead of simple branches inside getproperty, and it’s probably a bit harder to read too.

One detail, though.

Your fallbacks could be simpler, I think they don’t need the type signatures:

get(sub::FooSubtype, s) = getfield(sub, s)
set!(sub::FooSubtype, s, x) = setfield!(sub, s, x)
1 Like

Here is a version I tested that only requires extending a top level type Object and having a field super in all the subtypes. It will recurse up the type hierarchy searching for the property:

abstract type Object end

function Base.getproperty(o::Object, s::Symbol)
    t = typeof(o)
    if hasfield(t, s) 
        return getfield(o, s)
    elseif hasfield(t, :super)
        return getproperty(getfield(o, :super), s)
    end
end

function Base.setproperty!(o::Object, s::Symbol, x)
    t = typeof(o)
    if hasfield(t, s) 
        setfield!(o, s, x)
    elseif hasfield(t, :super)
        setproperty!(getfield(o, :super), s, x)
    end
end

I don’t think this test should be done. t should have a field called super, and if it doesn’t, you should get an error. Right now, it would just do nothing.

And isn’t it a bit overly verbose? I think this should work as well:

function Base.getproperty(o::Object, s::Symbol)
    if hasfield(typeof(o), s) 
        return getfield(o, s)
    end
    return o.super.s
end

function Base.setproperty!(o::Object, s::Symbol, x)
    if hasfield(typeof(o), s) 
        setfield!(o, s, x)
    else
        o.super.s = x
    end
end

Ok so here is the final working version that has been tested fairly well:

abstract type Object end

function Base.getproperty(o::Object, s::Symbol)
    if hasfield(typeof(o), s) 
        return getfield(o, s)
    end
    return getproperty(getfield(o, :super), s)
end

function Base.setproperty!(o::Object, s::Symbol, x)
    t = typeof(o)
    if hasfield(t, s) 
        setfield!(o, s, convert(fieldtype(t, s), x))
    else
        setproperty!(getfield(o, :super), s, x)
    end
end