Forward all function calls on ::MyType to another wrapped field

In Julia we cannot inherit from a concrete type. The advice often given to overcome this limitation is to use type composition, i.e. instead of making a type B a subtype of a concrete type A, wrap an instance of A in B, and then extend B with any extra field you need:

struct A end
struct B
   a::A
   some_extra_fields
end

The problem is that now I would want any function that has been defined on A to just work on B, by forwarding the wrapped instance. So I would want something like this:

julia> (f::Function)(b::B, args...) = f(b.a, args...)
ERROR: Method dispatch is unimplemented currently for this method signature
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

So that doesn’t work. Note that a custom Abstract type instead of Function does work.

julia> abstract type MyAbstractType end

julia> (::MyAbstractType)(args...) = 1

So my question is twofold:

  • Am I “holding it wrong”?
  • If this is indeed the intended approach for concrete inheritance, is there a technical reason I cannot extend the methods of abstract type Function for my B type, so I don’t need to define forwarding methods for each and every function that accepts the wrapped A?

(I’m sure there is a technical reason, but then it would be really interesting to know if this limitation could potentially be lifted in the future)

If that was possible, how would B have a different behavior than A? You wouldn’t be able to call even getfield on B, and thus the extra fields would not be accessible.

1 Like

You can however use @eval to do this automatically with a finite, manually specified list of functions

2 Likes

Oooh, good point about getfield. That is a mess indeed.
Other than getfield, I would expect that any new method defined specifically for B (myfunc(::B) = ...) would override the less specific (f::Function)(::B)

I know, but that’s precisely what I want to avoid, needing to fish functions one by one to put them in an @eval loop. I would need to remember to update this list with any new function I define.

I attempted this once before and my conclusion was it’s basically not possible

1 Like

I don’t know if it is feasible in your case, but you could subtype both A and B on an abstract AorB, and dispatch on it. Then redefine what getfield means for B such that the properties of A are accessible with the same syntax.

Maybe the getfield issue could be solved by making any (f::Function)(...) = definition be the least specific of all possible definitions, respect to method resolution.

If you are fine with doing some of the work manually you could use TypedDelegation.

I myself have used it for a while for a project but found it too onerous after a while. I’m now using CompositeStructs instead to simply glue the element structs together. This has its own issues (notably in combination with constructors and parametric types), but it’s a workable solution for now.

In general I understand why people advocate composition over inheritance. However, I think there is one use-case that is really difficult to solve (nicely) with composition alone and that’s when a library wants to provide combinable building blocks which can easily be assembled into new structures.

1 Like

I don’t know if it is feasible in your case, but you could subtype both A and B on an abstract AorB, and dispatch on it. Then redefine what getfield means for B such that the properties of A are accessible with the same syntax.

The problem is that I don’t own A

You can also dispatch on Union{A,B} and do not use the abstract type.

Mmm, I admit I don’t see how that solves the “forward any function” problem…

EDIT: clarification, I don’t own A or the (large) collection of methods f(::A)

Oh, I see, I thought you wanted the functions you defined for your new B type worked for the not-owned A type.

Something like:

julia> using StaticArrays
       
       struct B
           b::Float64
           vec::SVector{2,Float64}
       end
       
       import Base: getindex
       getindex(b::B, i::Int) = b.vec[i]
       
       const MyTypes = Union{<:SVector, B}
       
       function f(b::MyTypes)
           b[1] + b[2]
       end
f (generic function with 1 method)

julia> f(b)
2.0

julia> f(SVector{2,Float64}(1.0, 1.0))
2.0

The other way around I don’t think it is possible, in particular if the method signatures are specific for A.

Exactly, that’s the point!

Maybe adding the low-precedence sort of definition of (f::Function)(...)=... to Julia, as a new feature, could be possible. It would solve a well defined problem with no current solution. I still don’t know if there is a technical problem with this, or if it is a bad idea for any particular reason…

EDIT: A worry, would this cause invalidations on a massive (maybe catastrophic?) scale?

This seems like a bad idea. If that worked, then literally every single function in the Julia ecosystem would have a method definition for your wrapper type B. The meaning of a type is defined by its behavior, and its behavior is defined by its methods. What does it mean for a type to have a method for literally every single generic function in existence?

1 Like

Well, if we just forward the wrapped field to f, we are simply inheriting the behavior of A, not implementing arbitrary new behavior (we would get a MethodError if f(::A, ...) is not defined).

However, your comment indeed suggests that (f::Function)(b::B, args...) = ... is probably not the correct implementation of behavior inheritance. It should probably be done differently, not through a “abstract Function method”, but something like @forward from MacroTools.jl, but that could somehow work for any function. So

@forward B.a

would have the restricted effect of forwarding f(b::B, args...) to f(b.a, args...) for any function f

some other resources: GitHub - curtd/ForwardMethods.jl: Composition made easy(ish)

Thanks @jling , I didn’t know this package. The @forward_interface macro is particularly interesting, since it goes about solving this problem through the proper definition of interfaces, i.e. a named collection of function names that one wants to forward.

It does seem like a more sane approach than the “forward anything” idea that I was asking for, which was also @CameronBieganek 's point. So this feels to me like a hole in the design of the Julia type system, which may require a “Taking Interfaces Seriously” github megaissue to get fixed…

As long as the wrapped type has a well defined and reasonably simple interface, then all you need to do is forward the interface methods to the wrapped object and everything else will just work. Here’s an example:

struct NamedVector{T} <: AbstractVector{T}
    name::Symbol
    x::Vector{T}
end

Base.size(v::NamedVector) = size(v.x)
Base.getindex(v::NamedVector, i) = v.x[i]
Base.setindex!(v::NamedVector, val, i) = (v.x[i] = val)
Base.IndexStyle(::Type{<:NamedVector}) = IndexLinear()

Now all this stuff “just works”:

julia> v = NamedVector(:hello, [3, 2, 1])
3-element NamedVector{Int64}:
 3
 2
 1

julia> map(sqrt, v)
3-element Vector{Float64}:
 1.7320508075688772
 1.4142135623730951
 1.0

julia> sort(v)
3-element Vector{Int64}:
 1
 2
 3

So the goal is not to forward all 100 of the functions that work on A. The goal is to forward just a few functions that together completely define the behavior of A.

(Well, this particular example also relies on making the wrapped type a subtype of AbstractVector, but the principle is basically the same.)

1 Like