Getters & Setters in Julia

Decorative image of labels “getters” and “setters” above a code editor.

Julia’s dynamic multiple dispatch is an incredibly powerful language feature. It also powers retrieving fields of objects through getfield, setfield!, hasfield, and various other functions. Some versions ago (somewhere around version 1.1), these methods were complemented with “property” variants— which allow us to implement custom behavior upon accessing (getting) or overriding (setting) the value of a field, or even more interesting: virtual fields which do not physically exist on the object.

An Example

Nothing goes over learning by example. This is how you might implement getters & setters:

abstract type Animal end
abstract type Quadruped <: Animal end
struct Dog <: Quadruped
species::String
end
function Base.getproperty(qp::Quadruped, prop::Symbol)
if prop === :legs
return 4
else
return getfield(qp, prop)
end
end
function Base.setproperty!(qp::Quadruped, prop::Symbol, value)
if prop === :legs
error("needs specialization")
else
return setfield!(qp, prop, value)
end
end
Base.propertynames(qp::Quadruped) = tuple(fieldnames(qp)..., :legs)

Unfortunately, as one may see, these functions come at two rather large inconveniences: A) one must implement exactly three distinct functions, and B) these three functions destroy the principle of “separation of concerns.” When adding or removing a single property, a developer must change all three functions; the respective lines of code may be several dozen lines apart. This renders getters & setters in Julia inherently human-error prone. Furthermore, this approach is rather static and cannot be augmented in retrospect.

A better solution

I am an advocative fan of Julia’s meta programming. It is an incredibly powerful tool which once again offers a cleaner and more flexible solution. Instead of maintaining three distinct functions, we may instead simply write three generic implementations which delegate to additional functions. To this end, one may leverage templates like such:

struct Ident{S} endabstract type Animal end
abstract type Quadruped <: Animal end
struct Dog <: Quadrupend
species::String
end
Base.getproperty(qp::Quadruped, prop::Symbol) = delegate_getproperty(Ident{prop}(), qp)Base.setproperty!(qp::Quadruped, prop::Symbol, value) = delegate_setproperty!(Ident{prop}(), qp, value)Base.propertynames(qp::Quadruped) = tuple(fieldnames(qp)..., :legs)delegate_getproperty(::Ident{:legs}, qp::Quadruped) = 4
delegate_setproperty!(::Ident{:legs}, qp::Quadruped) = error("needs specialization")
delegate_setproperty!(::Ident{:legs}, dog::Dog) = error("Cannot reassign legs")

The meta type Ident{S} allows specializing delegate_getproperty and delegate_setproperty! for individual properties. Thankfully, the Julia compiler is very smart, and likely inlines these function calls, reducing the performance impact to virtually none. Unfortunately, this does not work so easily for Base.propertynames.

Enter Macros

This is where Julia’s macro system comes into play! It is quite likely a developer will have encountered this handy language feature before, and even inadvertently employed it. An early example is @enum, or even @spawn and @sync / @async.

One may employ macros to generate all of the above functions with much simpler syntax. However, macros are an entirely different beast which would blow the scope of this little post out of proportion. At this point, I shall simply refer to my library, which does just that: GenerateProperties.jl. Feel free to explore its open source code.

Employing my library, one would write code as such:

using GeneratePropertiesabstract type Animal end
abstract type Quadruped <: Animal end
struct Dog <: Quadruped
species::String
end
@generate_properties Quadruped begin
@get legs = 4
@set legs = error("cannot reassign legs")
end

This generates all Base.getproperty, Base.setproperty!, and Base.propertynames functions, properly adding the virtual ‘legs’ property to the list of property names without having to separately list it AND the syntax looks nice and clean. Hurrah!

Unfortunately, currently I have only added support for simple types. It is not yet possible to generate properties for both Quadruped and Dog at the same time, but I do intend to add this mechanic later. Alternatively, I’d gladly accept a pull request. ;)

Freelance Software Engineer | DLT/Blockchain Enthusiast & Future Entrepreneur

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store