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