[ANN] Announcing ForwardMethods.jl

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!

14 Likes

Version 1.3.1

Version 1.3.1 of ForwardMethods.jl is out now!

@define_interface

I’ve added a new macro for defining generic (i.e., apply to any Struct type) interfaces that don’t necessarily correspond to forwarding methods to a single field in the struct. The currently supported interfaces are

  • the getfields and setfields interfaces: Given x::T, these interfaces define $field(x) = getfield(x, $field) and $field!(x, v) = setfield!(x, $field, v) for each field in fieldnames(T), respectively. This allows you to write, for instance,
mutable struct A 
  key1::String
  key2::Int
end

@define_interface A interface=(getfields, setfields)
a = A("a", 0)
key1(a)
key1!(a, "b")
key1(a)

key2(a)
key2(a, 1)
key2(a)

Which can used to present a more easily extendable interface to your types compared to the built-in getproperty/setproperty! methods.

  • properties interface: Given x::T with struct-type subfields y1::S1, …, yn::SN, this interface defines Base.getproperty(x, k) and Base.setproperty!(x, k, v) such that when k in fieldnames(Sk), the corresponding method is forwarded to yk. This allows you to treat types composed of sub-types in a uniform way, e.g.,
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))
  • equality interface: Given x::T, defines Base.:(==) (or Base.isequal) in the obvious way, i.e., as
Base.:(==)(x::T, y::T) = all( getfield(x,k) == getfield(y,k) for k in fieldnames(T))
5 Likes