I’ve been writing Julia for a while, and I find that I often want to have a function dispatch on the type that’s in a collection, where I don’t really care what type of collection is used, be it a set or a vector or a tuple. I think I’ve found a solution, and I wanted to ask whether that solution is something I should actually use.
I looked for whether this was a thing, but I couldn’t find it. So I implemented it myself, using the Holy traits trick with parametric trait types, like so:
abstract type CollectionTrait{T} end
struct NonCollection{T} <: CollectionTrait{T} end
struct Collection{T} <: CollectionTrait{T} end
# Everything by default is not a collection
collection_trait(::Type{T}) where T = NonCollection{T}()
# Vectors, Sets, and Tuples are all collections
collection_trait(::Type{<:AbstractVector{T}}) where T = Collection{T}()
collection_trait(::Type{<:AbstractSet{T}}) where T = Collection{T}()
collection_trait(::Type{<:Tuple{Vararg{T}}}) where T = Collection{T}()
# dispatch to the specific methods
foo(x::T) where T = foo(collection_trait(T), x)
# The methods, where we now don't need to care what the specific collection type is
foo(::Collection{Symbol}, coll) = println("A collection of Symbols.")
foo(::Collection{<:Number}, coll) = println("A collection of Numbers.")
foo(::NonCollection, thing) = println("Just a thing, not a collection.")
Where on the last lines you see something that’s pretty much what I wanted.
So now my question is: should I use this? Has someone perhaps done a better job of implementing something like this and should I use their solution instead? Or is this an abstraction that I’d better not make, and should I just dispatch on Any and see if looping over the values works?
Why not simply do foo(collection) = bar(collection, eltype(collection)) with bar containing the implementation that may depend on both the type of collection and the type of its elements?
Basically, I believe that your trait may already be implemented as eltype. But please let me know if I misunderstood something!
That can work, but it does have different behaviour. With
bar(coll) = bar(coll, eltype(coll))
bar(coll, ::Type{Symbol}) = "A collection of symbols."
bar(coll, ::Type{<:Number}) = "A collection of numbers."
bar(thing, ::Type{<:Any}) = "Just a thing."
bar(thing, ::Type{Symbol}) = "Just a Symbol."
we get that bar([1, :a]) returns "Just a thing." (while with my trait that gives a MethodError), bar(3) returns "A collection of numbers", because apparently basically everything has an eltype which can just be itself, and bar(:a) returns "Just a thing." because eltype(Symbol) == Any.
With my CollectionTrait I can limit what’s a collection and what isn’t, while it can be extended by just adding another type.
I may have misinterpreted what you meant, though, of course.
I had missed that point, I thought you actually wanted it to generalize automatically to all collections (well, all that implement the documented interface as @Raf mentionned).
But if you want to be able to constrain the kind of collections to a known set, like only AbstractVector, AbstractSet and NTuple, and treat everything else differently, then I actually think your initial solution works pretty well!
(just a side note: actually it’s the other way round: everything has eltype equal to Any except collections, as well as numbers that have eltype(x) = typeof(x). That’s because numbers are considered themselves to be collections: you can iterate on a number and it will be considered a 1-sized collection containing only itself)