ForwardMethods provides macros that automate some of the boilerplate involved when using composition for object polymorphism. This package is essentially fancy copy + paste for forwarding function definitions, as well as providing automatically generated generic interfaces for struct types.
Given a hypothetical definition for type T with a subfield s of type S, i.e.,
struct T
...
s::S
...
endsuppose that type S has a number of existing functions defined as f₁(..., obj::S, ...; kwargs...), ..., fₙ(..., obj::S, ...; kwargs...). In the argument list here, ... indicates a fixed number of preceding or following arguments (and not a Julia splatting pattern).
If you wanted to define instances of these methods for x::T by forwarding them to getfield(x, :s), the normal boilerplate code you would have to write in standard Julia would be
for f in (:f₁, ..., :fₙ)
@eval $f(..., x::T, ...) = $f(..., getfield(x, :s), ...)
endwhich works fine when all of the functions fᵢ have same number of arguments and the position of the argument of type S is the same in each argument list. If this is not the case, it can be fairly tedious to generate all of the specific signature expressions to evaluate.
The @forward_methods macro automates much of this boilerplate away by only requiring you to specify the type, the fieldname, and the signature of the methods you'd like to forward.
To take a concrete example, suppose we define
struct T
s::Vector{Int}
endWe'd like to forward a number of method signatures corresponding to the AbstractArray interface. We can write this compactly using @forward_methods as:
@forward_methods T field=s Base.length(x::T) Base.getindex(x::T, i::Int) Base.setindex!(x::T, v, i::Int)Here the position of the argument of interest will be inferred from the type annotation ::T. We can write this even more compactly as
@forward_methods T field=s Base.length Base.getindex(_, i::Int) Base.setindex!(x::T, v, i::Int)Here a 0-argument expression Base.length is expanded to Base.length(x::T) and the placeholder underscore _ is expanded to x::T.
Rather than defining a forwarded method with an object argument to its child value, e.g., x::T -> getfield(x, :s) as above, you can also forward the method with a type argument ::Type{T} to fieldtype(T, :s) as follows
@forward_methods T field=s Base.eltype(::Type{T}) Base.IndexStyle(::Type{T})Parametric types and type signatures are also supported, so if you have, say,
struct A{B}
d::Dict{String, B}
endyou can easily forward parametric / non-parametric method definitions to field d simultaneously via
@forward_methods A{B} field=d (Base.getindex(a::A{B}, k::String) where {B}) Base.keys(x::A) Base.values(_)Letting x::T denote the object of interest, the value of the keyword argument field = _field has the following effects on the generated expressions:
-
If
_field = kwithk::Symbolor ak::QuoteNode, or an expression of the formgetfield(_, k), in which case methods will be forwarded togetfield(x, k) -
If
_field = a.b.c. ... .zis a dotted expression, methods will be forwarded togetfield(getfield(... getfield(getfield(x, :a), :b), ...), :z) -
If
_fieldis an expression of the formgetproperty(_, k), the method instances will be forwarded togetproperty(x, k) -
If
_fieldis an expression of the formt[args...], the method instances will be forwarded tox[args...] -
If
_fieldis an expression of the formf(_)for a single-argument functionf, the method instances will be forwarded tof(x)
If the type of S has a known interface (e.g., a fixed set of methods defined with a particular type signature), it may be more convenient to forward the entire suite of methods for that interface to objects x::T, rather than to specify each method signature individually.
@forward_interface T field=_field interface=_interface [kwargs...]Here T and _field are as above and _interface is one of a preset number of values, namely iteration, indexing, array, dict, getfields, and setfields. The value of _interface determines the specific forwarded method signatures that are generated, e.g.,
struct B
b::Dict{String, Int}
end
@forward_interface B field=b interface=dict
b = B(Dict{String,Int}())b can now be used as a drop-in replacement in any method where a Dict{String,Int} is supported.
Note: certain methods for certain interfaces (e.g., Base.similar for the Array interface) are not included in this macro as direct method forwarding would not make sense to apply in these cases.
The getfields and setfields interfaces are dynamically generated based on the fields of type T.
When interface=getfields, this macro forwards methods of the form $field(x::T) = getfield(x, $field) for each field ∈ fieldnames(T)
When interface=setfields, this macro forwards methods of the form $field!(x::T, value) = setfield!(x, $field, value) for each field ∈ fieldnames(T)
Certain interfaces are defined for objects x::T that don't involve explicit forwarding to a fixed-field, per-se, but can be generally useful.
The properties interface allows a unified Base.propertynames, Base.getproperty, and Base.setproperty! interface for x::T which is composed of subfields k1, k2, ..., kn whose fields should also be included in the properties of x. This pattern arises when creating composite types. For example,
julia> struct A
key1::Int
key2::Bool
end
julia> struct B
key3::String
key4::Float64
end
julia> struct C
a::A
b::B
end
julia> @define_interface C interface=properties delegated_fields=(a,b)
julia> c = C(A(1, true), B("a", 0.0))
C(A(1, true), B("a", 0.0))
julia> (key1=c.key1, key2=c.key2, key3=c.key3, key4=c.key4, a=c.a, b=c.b)
(key1 = 1, key2 = true, key3 = "a", key4 = 0.0, a = A(1, true), b = B("a", 0.0))Essentially the fields of c.a and c.b has been flattened to provide a unified view of the properties of c.
The equality interface defines Base.== or Base.isequal for objects of T in the obvious way, i.e.,
Base.:(==)(x::T, y::T) = all( getfield(x,k) == getfield(y,k) for k in fieldnames(T) )There are some configurable options for this macro, so you can do some fancier equality comparisons such as
julia> struct E
d::Dict{Symbol,Int}
end
julia> Base.propertynames(e::E) = collect(keys(getfield(e, :d)))
julia> Base.getproperty(e::E, k::Symbol) = getfield(e, :d)[k]
julia> Base.setproperty!(e::E, k::Symbol, v::Int) = getfield(e, :d)[k] = v
julia> @define_interface E interface=equality compare_fields=propertynames
julia>
julia> e = E(Dict(:a => 1))
E(Dict(:a => 1))
julia> e == E(Dict(:a => 1))
true
julia> e == E(Dict(:a => 2))
false- ReusePatterns.jl
@forwardin Lazy.jl