I’m happy to announce ForwardMethods.jl v1.1, a package made to help remove some boilerplate when defining forwarded/delegated methods.
Method Definition Forwarding
Concretely, suppose you have a composite type
struct A
d::Dict{String,Any}
end
If you’re looking to delegate or forward Dict
methods from an object x::A
to x.d
, a standard approach in Julia would be to write
for f in (:length, :isempty, :empty!)
@eval Base.$f(x::A) = $f(getfield(x, :d))
end
which works fine if all of the functions you’re interested in forwarding have a) the same number of arguments and b) the position of x
in the argument list remains constant. When forwarding a larger number of methods with a greater heterogeneity in their argument list, it can be annoying to keep track of a large number of method signatures, especially if you’d like much more control over the type signatures involved in the forwarded methods.
The @forward_methods
macro allows you to avoid some of these issues by writing
@forward_methods A field=d Base.length(x::A) Base.isempty(x::A) Base.getindex(x::A, k) Base.setindex!(x::A, v, i)
or, even more compactly,
@forward_methods A field=d Base.length Base.isempty Base.getindex(_, k) Base.setindex!(x::A, v, i)
Here 0
-argument expressions are implicitly expanded to a 1
-argument function and _
is shorthand for x::A
.
The field
keyword argument supports a number of different parameters including nested dotted expressions, which forward to deeply nested structures, e.g.,
@forward_methods A field=b.c.d Base.length
will forward Base.length(x::A)
to Base.length(getfield(getfield(getfield(A, :b), :c), :d))
.
@forward_methods
will also handle argument signatures with Type
arguments, e.g.,
@forward_methods A field=b.c.d Base.eltype(::Type{A})
will define
Base.eltype(::Type{A}) = Base.eltype(fieldtype(fieldtype(fieldtype(A, :b), :c), :d))
Interface Forwarding
Similarly, the @forward_interfaces
macro allows you to forward entire interfaces (i.e., named collections of particular method signatures) rather than have to write out/remember each method signature by hand.
You can also post-apply an expression to the resulting forwarded method via the map
keyword argument, which allows you to do some nice fancy things like
struct LockableDict{K,V} <: AbstractDict{K, V}
d::Dict{K,V}
lock::ReentrantLock
end
@forward_interface LockableDict{K,V} field=lock interface=lockable
@forward_interface LockableDict{K,V} field=d interface=dict map=begin lock(_obj); try _ finally unlock(_obj) end end
d = LockableDict(Dict{String,Int}(), ReentrantLock())
And now all of your standard dictionary calls like e.g., d["a"] = 0
will be thread-safe! (And horribly non-performant, but that’s not the point here).
This package is basically just a fancy copy+paste system but takes into account the syntax of Julia method signatures. Hopefully it makes using the Composite design pattern easier to implement for your types.
Happy forwarding!