Multiple dispatch, but for structs?

I was thinking about having an option to specify a “fallback” usage for structs, something that seems to me like a reversed multiple dispatch?

Let’s say I have a struct:

struct Foo
    x::String
    y::Array
end

And I would like to specify, that if someone would use a function on it that does not have specialized version for Foo but has for e.g. AbstractArray it would then use Foo.y instead of Foo.
I was not sure how to call this, so I haven’t found much with searching Discourse/Google. Can we do it somehow in Julia?

1 Like

I think your question here relates to “forwarding” or “delegating” functions to a field when using a “composition” design pattern.

There have been numerous discussion about this here on discourse; maybe the most comprehensive list can be found at the end of the ReusePatterns readme:

As to “how”, that same ReusePatterns package provides this kind of feature, and its readme also gives a detailed list of packages that provide related features.

5 Likes

Although you can dispatch on the type of the callable, even abstract ones, instead of specifying a const name, Julia disallows (f::Function)(a::Foo) = f(a.y) because it would allow call signatures too broadly, even paradoxically involving functions that aren’t multimethods. You would have to specify the functions individually, like bar(a::Foo) = bar(a.y) or a general fallback bar(a) = bar(a.y). The forwarding macros can make such methods in a nicer syntax especially for multiple listed functions, and some can search and implement all applicable functions like the previously linked ReusePatterns, though with common-sense limitations. AFAIK these macros all share the limitation of accessing a specific field name, so they can’t do something broader like bar(a) = bar(getarray(a)). They also cannot automatically rewrap types, like bar(::Foo)::Foo to reflect bar(::Array)::Array, because that sort of processing will be different for different types. For example, if you matrix-multiply 2 Foos by forwarding to the arrays at the y field, what do you do about the 2 strings at the x field?

Personally I prefer to implement getarray(a::Foo) = a.y with a fallback getarray(a::AbstractArray) = a (if the types are semantically views of an array, then Base.parent should be used and already has that fallback), and I would call bar(getarray(b)) instead of adding methods to make bar work on types other than arrays. Although extending functions on new types is normal, I don’t often find forwarding alone to be enough justification. That’s just a subjective style, and it’s not less work than forwarding, especially with those convenient macros.

1 Like

Yes, forwarding seems to be exactly what I had in mind, thanks!

@Benny not sure if we are thinking of the same scenario - my use case would be having a data structure that would behave as a dense array unless stated otherwise, so a user could call e.g. eigen and just get the result. But the obstacle would be that I don’t know which functions user would want to use, so could not extend them in my code.
So the question is can I make the struct compatible with array functions while only having control of the said struct and not implementing any specialized functions.

Well, no, you do have to implement methods on your type to make anything happen, that’s what forwarding does, too. It’s ambiguous what you mean by “behave as a dense array”; that could be something different from forwarding.

If you want your type to directly represent a dense array, not just contain one, I’ll suggest 2 options: 1) if the elements are stored contiguously, which y::Array{T,N}* would be, then subtype struct Foo{T,N} <: DenseArray{T,N}, or 2) if dense storage isn’t critical, subtype struct Foo{T,N} <: AbstractArray{T,N} (note that DenseArray is a subtype of AbstractArray). *The parameters will be important for dispatching to existing AbstractArray methods, and you should match it to the y field. Whichever option you take, you should then implement some methods in the AbstractArray interface to let base AbstractArray methods work on your type, and yes they should forward to the contained y::Array{T,N} in some form. Which optional interface methods to implement depends on your use case (if you don’t want mutation, you shouldn’t implement setindex!), and some of them have good default implementation (like iterate being based on the required getindex). Earlier I said that I try to leave a function alone and explicitly call a getter instead of extending that function just for forwarding, and implementing an interface used by that function and likely many more others is another way to leave those functions alone, except you do extend the smaller set of interface methods.

Also, if you decide to go the actual array route, then you should implement Base.parent(a::Foo) = a.y if you want a getter, don’t make your own.

Yeah, so I guess I was thinking about something that would behave as subtyping, but for multiple types, not only AbstractArray, but also e.g. AbstractString. But the more I thought about it, the more apparent it was, that it would generate a lot of ambiguities.
I actually tried subtyping from AbstractArray but at the time it didn’t fully work for me (but it was pretty long ago and I must have done something wrong there). Will have to give it another try.

On the side note, it seems to me, that forwarding would be now doing a very similar thing to package extensions, as you have to generate methods on your own. Is there any advantage to forwarding over them?

Yeah multiple supertypes don’t mix well with multimethods, so Julia doesn’t have them. You can sort of get around it with multiple traits, where a type gets 1 trait per trait-method. But honestly for this I would just dispatch on a.x or a.y, “composition over inheritance” as it’s said.

I don’t see the parallel you are drawing. In both you are defining methods, yes, but that seems true for implementing anything. Package extensions are about mixing functions and types from multiple packages in methods that get loaded automatically when all of said packages are loaded. You could make forwarding methods in them, but that’s an orthogonal option.