As of version 1.4 Julia does not support property inheritance. However, many other languages such as Python do support inheritance and porting libraries from these languages becomes difficult.
I am proposing an informal design pattern to enable type inheritance for this purpose. The goals are to:
- Not require external dependencies
- Minimize boilerplate as much as possible
- Maintain dot-syntax property accessors
- Follow encapsulation principals. That is changes to one type should only require modifying that one type and should not require having to update subtypes or cause a cascade of changes.
Please provide feedback if you think this method is flawed, unnecessary, or needs improvement.
For each concrete type Foo
there should be two abstract types associated with it AbstractFoo
and FooSubtype
.
The AbstractFoo
represents Foo
and all if it’s subtypes. Methods of Foo
should have AbstractFoo
as their first parameter.
FooSubtype
represents only the subtypes of Foo
. This is used to create property getters and setters for the subtypes.
Here is the structure of this pattern:
abstract type AbstractFoo end
mutable struct Foo <: AbstractFoo
a::String
Foo(a) = new(a)
end
say_something(foo::AbstractFoo) = println(foo.a)
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
abstract type AbstractBar <: FooSubtype end
mutable struct Bar <: AbstractBar
foo::Foo
b::String
Bar(a, b) = new(Foo(a), b)
end
say_something(bar::AbstractBar) = println("$(bar.a) $(bar.b)")
abstract type BarSubtype <: AbstractBar end
Base.getproperty(sub::BarSubtype, s::Symbol) = get(sub, Val(s))
# In a sub-type, a getter to the super-type object is defined
get(sub::BarSubtype, ::Val{:foo}) = sub.bar.foo
get(sub::BarSubtype, ::Val{:b}) = sub.bar.b
Base.setproperty!(sub::BarSubtype, s::Symbol, x) = set!(sub, Val(s), x)
set!(sub::BarSubtype, ::Val{:b}, x) = sub.bar.b = x
abstract type AbstractBaz <: BarSubtype end
mutable struct Baz <: AbstractBaz
bar::Bar
c::String
Baz(a, b, c) = new(Bar(a, b), c)
end
say_something(baz::AbstractBaz) = println("$(baz.a) $(baz.b) $(baz.c)")
abstract type BazSubtype <: AbstractBaz end
Base.getproperty(sub::BazSubtype, s::Symbol) = get(sub, Val(s))
# In a sub-type, a getter to the super-type object is defined
get(sub::BazSubtype, ::Val{:bar}) = sub.baz.bar
get(sub::BazSubtype, ::Val{:c}) = sub.baz.c
Base.setproperty!(sub::BazSubtype, s::Symbol, x) = set!(sub, Val(s), x)
set!(sub::BazSubtype, ::Val{:c}, x) = sub.baz.c = x
foo = Foo("Foo")
say_something(foo)
bar = Bar("Foo", "Bar")
say_something(bar)
baz = Baz("Foo", "Bar", "Baz")
say_something(baz)
baz.a = "FOO"
baz.b = "BAR"
baz.c = "BAZ"
say_something(baz)