Since we can’t define fields on abstract types (at least yet, see this very long discussion), I was wondering whether the following design pattern is considered good practice in Julia:
abstract type A end
struct B <: A
x::Int
end
getx(a::A) = a.x
I’m asking because if the following struct is later defined:
One aspect to keep in mind is that Julia uses a functional approach with multiple dispatch, rather than object oriented programming. Therefore, some of the principles from OOP don’t translate well into Julia.
A common approach in Julia is to work with so-called interfaces. E.g. informally defined sets of methods each subtype is expected to have. You are right that this can lead to a bit more code here and there, but it brings other advantages (well explained in the video I linked below.)
It is not so common to write getter/setter methods in Julia. If you do so, often getproperty/setproperty are your friends. In your case, that could be that you (for yourself) make the rule that each subtype of A should have a well-defined behaviour for getproperty(a, :x).
Notice that multiple dispatch gives you more freedom (which has advantages and disadvantages; it’s just a different approach ). Here is a quick example:
abstract type A end
two_times_x(a::A) = 2*a.x
x_squared(a::A) = a.x^2
mutable struct B <: A
x::Int
end
mutable struct C <: A
x²::Int
end
Base.getproperty(c::C, s::Symbol) = s == :x ? Int(sqrt(c.x²)) : getfield(c, s)
Base.setproperty!(c::C, s::Symbol, v) = s == :x ? (c.x² = v^2) : setfield!(c, s, v)
x_squared(c::C) = c.x²
c = C(64)
@show c.x # == 8
c.x = 9 # uses setproperty
@show c.x # == 9
b = B(8)
@show b.x # == 8
two_times_x(b), two_times_x(c) # == (16, 18)
x_squared(b), x_squared(c) # == (64, 81)
As you can see, you have the freedom the implement subtypes which don’t even have the field x but maybe just an abstract representation of the data. But externally they still behave compatible.
Just to wrap things up and make sure I understood what you said so I can mark your answer as a solution:
Sometimes part of the interface of a type involves getting access to one of its properties.
For example, if I have an abstract type Vehicle I might want to include in my interface a method model(v::Vehicle) that, for most concrete Vehicles, will just access a field in the struct. Not necessarily all concretes of Vehicle have this field, however.
Therefore I could define my interface as such:
abstract type Vehicle end
model(v::Vehicle) = v.model
struct Car <: Vehicle
age::Int
model::String
end
struct Bus <: Vehicle
age::Int
model::String
end
# Not worth saving the model as a property, I know its the same for every instance of the struct
struct ToyotaCorolla <: Vehicle
age:Int
end
model(tc::ToyotaCorolla) = "Corolla"
Is this reasonable or should I not “rely” of the field access and instead go for:
abstract type Vehicle end
struct Car <: Vehicle
age::Int
model::String
end
model(c::Car) = c.model
struct Bus <: Vehicle
age::Int
model::String
end
model(b::Bus) = b.model
struct ToyotaCorolla <: Vehicle
age:Int
end
model(tc::ToyotaCorolla) = "Corolla"
abstract type Vehicle end
model(v::Vehicle) = v.model
is ok if you are the author of all the subtypes of Vehicle, but if other people will be extending the Vehicle interface with their own subtypes of Vehicle, then it is better to use your second option above. Julia interfaces typically do not require subtypes to have specific fields.
For instance, suppose I define my own Vehicle subtype like this:
struct HondaCivic <: Vehicle
age:Int
end
If I forget to extend model for HondaCivic, I will get an ERROR: type HondaCivic has no field model when model is called on a HondaCivic. But it is more idiomatic to get a MethodError if a method is not implemented for a type. In this case, model is in fact implemented for HondaCivic via the model(v::Vehicle) default method—it just happens to be incorrectly implemented for HondaCivic.
Methods on abstract types make more sense if they only call generic functions on their arguments. Here’s an example:
abstract type AbstractRectangle end
area(r::AbstractRectangle) = len(r) * wid(r)
struct Rectangle <: AbstractRectangle
length::Float64
width::Float64
end
len(r::Rectangle) = r.length
wid(r::Rectangle) = r.width
struct Square <: AbstractRectangle
width::Float64
end
len(s::Square) = s.width
wid(s::Square) = s.width
Note that I have to give concrete definitions of len and wid for each of the concrete subtypes of AbstractRectangle.
Note that you are almost never doing that, unless you use getfield explicitly. The . syntax is lowered to getproperty, which happens to fall back to getfield by default. But it does not have to.
The only slightly cumbersome thing about getproperty is that it takes a value, so you cannot add “methods” to it. For simple cases, people use an if (see the manual), but if you insist you can do
may just signal a broader issue with your API. If most structs are “containers”, then accessing their fields directly is fine IMO, and working around the occasional exception with getproperty etc is OK. But if a nontrivial type hierarchy has the same pattern and you need a lot of extras (calculating fields, checking valid values, etc), then exposing field access with a function could be an idiomatic choice. YMMV.