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

This is not good, there’s no way to find what are the methods that “define the behavior of A”. I’m having the same problem of the OP, the issue is the lack of inheritance. i would like to do something like:

using DataFrames

struct MyDataFrame <: DataFrame end

# override the Base.show implementation
function Base.show(io::IO, mime::MIME"text/plain", df::MyDataFrame)
    show(io, mime, DataFrame(df); allcols=true, allrows=true, eltypes=false)
end

# the constructors should be "inherited"
df = MyDataFrame([...])
typeof(df) # -> MyDataFrame

display(df) # -> calls my custom defined show() above

But this doesn’t work.