Give Attribtures the treatment they deserve: use dynamic dispatch to implement an extensible mechanism for user provided attributes.
Julia 0.7+ makes it possible to overload property access via Base.getproperty
and Base.setproperty!
, and uses Symbols
to name a property. This makes it
easier to use in dynamic scenarios (e.g. Python interoperability), but has one
huge drawback: all the coded related to properties for a single type has to be
in one single place and can thus not be extended. This is not the Julian way of
using dynamic dispatch to get the usual extensibility along multiple dimensions
we know and love!
There is another problem with getproperty/setproperty this package tries to adress: performance. Consider this code:
struct Foo
x::Int
end
function compute_y(a::Foo)
return a.x * 2 + 1
end
@inline function Base.getproperty(a::Foo, f::Symbol)
(f === :x) && return getfield(a, :x)
(f === :y) && return compute_y(a)
error("type $(typeof(a)) has no field $f")
end
f(a::Foo) = a.y
code_native(f, (Foo,))
# Output:
# .text
# [...]
# pushq %rax
# movabsq $julia_getproperty_36741, %rax
# movabsq $116219542556536, %rsi # imm = 0x69B3788CCF78
# callq *%rax
# [...]
# leaq 1(%rax,%rax), %rax
# popq %rcx
# retq
# nop
Why is getproperty
not inlined? The problem is getproperty calling compute_y
which itself calls getproperty
(after lowering a.x
). So the compiler
rightfully refuses to do recursive inlining (there are ways around it, e.g.
partial inlining, but lets appreciate what we have!). One fix is to replace
a.x
with getfield(a, :x)
to break the cycle in the call graph.
This package provides a similar solution via the @literalattrs
macro. This
macro replaces the property access with literal_getattr
and
literal_setattr!
, to avoid the cyclic call graph.
using Attrs
@defattrs Foo
@literalattrs function compute_y(a::Foo)
return a.x * 2 + 1
end
@inline Attrs.getattr(a::Foo, ::Attr{:y}) = compute_y(a)
code_native(f, (Foo, ))
# Output:
# .text
# movq (%rdi), %rax
# leaq 1(%rax,%rax), %rax
# retq
# nopl (%rax)
Now compute_y(a)
has been inlined!
First define your type as usual:
struct MyType
[...]
end
Make your type opt in to the Attrs
package (after using Attrs
):
@defattrs MyType
@defattrs MyOtherType{X, Y} where {X<:AbstractFloat, Y<:Integer}
Define your attributes, make sure all gettatr
/settattr!
methods of your type
use the @literalattrs
macro to make inlining possible.
@inline @literalattrs Attrs.getattr(x::MyType, ::Attr{:foo}) = [...]
@inline @literalattrs Attrs.setattr!(x::MyType, ::Attr{:foo}, y) = [...]
Now just use your type as usual: f(x::MyType) = x.y
no @literalattrs
is
necessary at this point!