Suppose a concrete type Foo is defined in another package that is unlikely to want any changes made to it. There are many methods defined using Foo.
I would like to create a type Bar that is essentially the same as Foo but has methods for a few new functions I have defined and may have different behaviour for a few existing functions.
My initial thought is to wrap a Foo:
struct Bar
x::Foo
end
I can then define my new functions/custom methods dispatching on Bar.
For all other functions, I can create a wrapper that forwards to Foo:
f(x::Bar, y::Real) == f(x.Foo, y)
but I’m assuming that there are many methods defined using Foo and it would be too much effort to write them all out (or new methods might be added at any point making this hard to maintain).
Is there a way to have this forwarding take place automatically. I.e., if no specific method is defined for x::Bar it calls the same method using x.Foo instead?
It may instead be helpful to know why this might be a bad idea and to be avoided.
There have been a few macros made for forwarding methods like this, often not even as a focus for the package e.g. @forward in MacroTools.jl. Not really sure why people hadn’t arrived at a centralized dependency, but it might be because people prefer to manually forward to it f(bar.x, y) when the wrapper doesn’t do any extra processing e.g. view(x, I)[i] != x[i]. It doesn’t need to be a strict field structure either, a function can generically get the Foo instance f(getFoo(bar), y) for a variety of composite types. An unwrapped Foo can still be “gotten” by an identity-like getFoo(x::Foo) = x. Of course, that involves its own busy work, and it’s only appropriate if Bar is to be processed to Foo. When Bar is supposed to behave like Foo, then you can’t get around defining methods for the same function, but (1) the aforementioned macros or (2) designing a smaller interface can cut down on the busy work.
My understanding with these forwarding macros is that you need to specify a list of functions you want forwarding to happen with, which is a bit hard to maintain. I assume there must be a reason, either performance or that it causes bugs why there is no way to do this for all functions as a default.
One issue may be that for all functions means also for any future functions and since that would have to be annotated at the struct definition, it can‘t affect any functions defined after that (e.g. in packages depending on that struct). For that, it would have to be supported at the language level forwarding all dispatches.
That’s impossible when insufficient information is provided. The closest thing to easy forwarding is inheritance (in other languages, Julia doesn’t have classes or concrete subtyping), but you’re still giving critical information to the language about subtyping and structure there. There’s no design where a method baz(f::Foo) can flout your manual type restriction to input a completely unrelated Bar, let alone know to forward another baz call to the nested Foo. It’s not hard to run into a problem: if Bar contains 2 Foo fields, there’s no logical choice for automatic forwarding.
If you want methods to automatically work on both Foo and Bar in Julia, then they can be annotated with an abstract supertype. It could be as simple as Union{Foo, Bar}, or you could make a named supertype FooLike in preparation for other Foo-like types. The getFoo strategy can work for arbitrary substructures (getters are recommended across languages for a reason), but alternatively you could design (and document!) FooLike subtypes to share properties rather than contain each other.