`@forward` on vectors

I recently learned about the “delegation” design pattern, and that there are numerous ways one could implement this within Julia. For example, the famous @forward macro makes it easy to “delegate” a set of methods meant for one struct to another wrapper struct.

Is there a canonical way to do the same for functions that work on a vector of structs? For example, suppose I have something like

struct foo
val::Int
end

and I want to extend the Base.findfirst() method to a vector of foos by looking at the val member.

Thanks!

You could certainly overload findfirst this way, but it seems like a confusing pun on the meaning of findfirst? Or you could define a whole bunch of methods like == and so forth that you want to work for comparing a struct foo to numbers?

Seems a lot clearer to just write things like findfirst(==(foo(3)), array_of_foos), though, unless struct foo is supposed to represent a kind of numbrer?

A lot of the motivation for some of these OOP “design patterns” goes away thanks to multiple dispatch, which allows you to add new methods to existing types or new types to existing methods more easily than in OOP languages.

2 Likes

Thanks, @stevengj, for your reply.

but it seems like a confusing pun on the meaning of findfirst ?

Yes, this was a bad example on my part. Let me try another example (using one your packages) that might better motivate my question. It may be that there’s a simple “multiple dispatch solution” to this as well (if so, great!) but I’m having trouble thinking through it…

Suppose I was building a system matrix with GeometryPrimitives.jl. The package exports an abstract Shape, along with various shape subtypes, like Cylinder, Cube, etc. Importantly, it also exports some useful functions, like KDTree, and even overloads findfirst to operate on a vector of Shape types.

Now suppose I wanted to create a “wrapper” type that had all the attributes of existing shapes, but also had a permittivity field (ε) that I could use to populate my system matrix (or maybe I include various other “material parameters”).

This seems like a prime use case for the “delegation design pattern” (from what I’ve read), as I could simply “forward” all of the existing infrastructure to my new wrapper type. I’m wondering if there’s a clean way to do this with the methods that act on vectors of types too.

But perhaps there’s an even simpler, more idiomatic way to accomplish this? Thanks!

In that particular case, Shape is an abstract type, so one approach would be to simply declare a subtype that adds metadata, but contains another shape and delegates to it for geometric primitives. Then it will work fine with array methods.

In the case of this particular package, I would caution you that we found in hindsight that working with a Vector{<:Shape} performs poorly. See this discussion on a related topic: Allocation and slow down when # of types involved increase (slower than C++ virtual methods) - #2 by jling … I’ve been meaning to re-work it with something like the approach in GitHub - YingboMa/Unityper.jl, emulating C union types to avoid the general case of dynamic dispatch.

I would also say that the KDTree implementation in that package might be better off returning an index into an array of shapes, so that you can simply store metadata about each shape in another array. In general, I think that this package needs some re-working and the difficulty of using it with metadata is a symptom of that.

2 Likes

Ah so simple. Thanks!

I would also say that the KDTree implementation in that package might be better off returning an index into an array of shapes

I agree. I actually have a version that does this. I can submit a PR if interested. (I even submitted an issue about this here).

In the case of this particular package, I would caution you that we found in hindsight that working with a Vector{<:Shape} performs poorly.

Yup I remember talking about this awhile back. Here’s another thread with additional details if anyone else is curious.

Thanks, @stevengj!