Developing Pass-Through Types

A few times I have run into this situation, I would like to make a new type that only has one field, for example,

struct Foo{T}
    bar::Vector{T}
end

The general case is that I would like instances of Foo to behave just like instances of Vector and I would like all of the methods that work on Vector to work on Foo (simply by using the value stored in bar). However, in a rare occasion, I would like to dispatch on the type Foo differently than on the type Vector.

Is there a Julia pattern for getting this sort of behavior?

2 Likes

Could you simply make struct Foo{T} <: AbstractVector{T}?

In this case wouldn’t Foo need to re-implement all of the implementation details that occur between an AbstractVector and a concrete Vector? Or have some generic pass-through method to delegate most function calls to its internal Vector?

The section of the manual on “Code Generation” within the chapter on “Metaprogramming” gives an example of how to quickly generate operators for a new type in terms of known operators. I haven’t ever used this technique, so I can’t vouch for it.

It would be nice to have an ArrayWrappers package that has a macro which passes on the functions calls. Probably any of the array packages would be a good starting point to get the list of functions that need to be overloaded.

I’ve found the @forward macro from Lazy.jl useful in cases like this: https://github.com/MikeInnes/Lazy.jl/blob/271bdc04a02aa0d99d87c17ffed41c3a07d4fd57/src/macros.jl#L248-L271

With @forward you still have to specify every function that you want to forward to the wrapped variable (so it’s not exactly what you’re looking for), but it still saves a fair amount of work.

1 Like

Yes, that’s a good one.

And there exists [TypedDelegation.jl]


import Base: show, string, signbit, sign, abs, div
 
 
struct DelegatedInt
    value::Int
end


# [show, string, signbit] evaluate as other types (not DelegatedInt)

@delegate_onefield( DelegatedInt, value, [show, string, signbit] )


# [sign, abs] evaluate as the given type (DelgatedInt)
                   
@delegate_onefield_astype( DelegatedInt, value, [sign, abs] )


# [div] takes two args and evaluates as the given type

@delegate_onefield_twovars_astype( DelegatedInt, value, [div] )

IMO that is the right trade-off for most cases, in terms of code readability/documentation. I can imagine a specialized solution for some interfaces (eg <: Number) which forwards a predefined set of functions, but for one-off solutions just enumerating is worth it when looking at that code 3 months later.

I do hope that one of the goals post v1.0 will be to come up with some standard interface / protocol design for Julia (I’ve seen it discussed many times, but I’m not sure what the latest thinking is).
I found it to be the biggest pain in writing a large (set of) packages for strings, that and trying to avoid ambiguity issues and type piracy.
Sometimes you need to extend an exported method, but other times you need to extend an unexported _* method (like _replace, _split, _rsplit) to get things to work, and it’s not really documented.
Finally, sometimes the only way to make things work so that things work as you’d expect is via completely replacing the Base structs and/or methods (that’s the case for regex support, because the macros such as @r_str create a Regex struct that is not generic, it only works for String)

1 Like

These are the fields you need to overload,
and this is how you overload them

https://github.com/JuliaText/CorpusLoaders.jl/blob/1a011a08b6072242604b6fd2ebcdf5ed258b6f44/src/types.jl#L5-L23

That one is done by declaring an abstract type.
But of-course you can do it directly,
or via metaprogramming