Is a parametric Collection trait useful?

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.

So I wanted it to work something like this:

foo(thing) = x
foo(coll::Collection{<:Number}) = coll .* 2
foo(coll::Collection{<:AbstractString}) = join(coll, ", "))

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!

1 Like

Half of what you want already exists, in Base.IteratorEltype. If it returns a HasEltype you can call eltype on the object.

But some objects are collections without known eltypes, or don’t implement iteration. Its a pretty loose “interface”.

2 Likes

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!

1 Like

(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)

1 Like