function f(collection::Vector{Int64})
for i in collection
println("Integer: $i")
end
end
function f(collection::Vector{String})
for s in collection
println("String: $s")
end
end
f([4, 5, 6]) # Success.
f(["a", "b", "c"]) # Success.
f((4, 5, 6)) # Failure: collection type is not Vector.
f([Substring("abc", i, i) for i in range(1, 3)]) # Failure: element type is not String.
To make the methods above more flexible to type dispatching, I wish I could write the following:
function f(collection::C) where eltype(C) <: Integer
for i in collection
println("Integer: $i")
end
end
function f(collection::C) where eltype(C) <: AbstractString
for s in collection
println("String: $s")
end
end
Unfortunately, the above is not valid Julia code (yet?). Is is possible to achieve this sort of “abstract covariant dispatching” I’m after? or “sophisticated static trait bounds”? If not, what’s a usual workaround? I would personally go for:
function f(collection)
if eltype(collection) <: Integer
f_for_integers(collection)
elseif eltype(collection) <: AbstractString
f_for_strings(collection)
else
throw(ArgumentError("Invalid type..."))
end
end
function f_for_integers(collection)
for i in collection
println("Integer: $i")
end
end
function f_for_strings(collection)
for s in collection
println("String: $s")
end
end
… but this feels like too much explicit type checking for a statically typed language, is it?
Moreover, the Julia package WhereTraits might meet your requirement.
julia> @traits function f(collection) where eltype(collection) <: Integer
for i in collection
println("Integer: $i")
end
end
julia> f([1,2,3])
Integer: 1
Integer: 2
Integer: 3
Note: There will have five methods for f by this way.
julia> methods(f)
# 5 methods for generic function "f":
...
If it is really matter to include tuples - you should take into account that !(Tuple <: AbstractVector), but isa((1,2,3), Tuple{Vararg{Int}}), so you should make method for tuple of strings:
function f(collection::Tuple{Vararg{<:AbstractString}})
for s in collection
println("String: $s")
end
end
Although this does work with the examples I have chosen, you can only generalize this approach to functions where do_scalar can be written (for example, functions with independent processing of every item). In other terms, this solution depends on the actual code within f in a way that sort of bypasses the actual dispatching problem ^ ^"
Yupe. But as you write, this does not dispatch over tuples or generators, only over collections in subtypes(AbstractVectors).
Not exactly, since I would also expect f(Set(range(5)) to work. This alternate collection type is neither a Vector nor a Tuple, but still, eltype(C) <: Integer. Is there no abstract julia type for collections and/or iterables?
This seems exactly powerful enough for my purpose Thank you! Can I first make sure that there is no built-in Julia idiom for that before I mark your post as the “solution”?
I suppose that using dispatch this way is not good idea by design. If you want to work only with collections you can check it applicable(length, [1,2,3])
If you want to dispatch by eltype you can try something like this:
function f(collection, ::Type{<:AbstractString})
for i in collection
println(i)
end
end
function f2(collection)
if applicable(length, collection)
f(collection, eltype(collection))
end
end
a = ["s","ss", "sss"]
f2(a)
I’d say your original solution is pretty idiomatic. The branching in f is basically cost free, or rather eliminated entirely, since eltype (should) constant propagate. I’m pretty sure @traits generates something very similar, but have a look with @macroexpand.
Can you elaborate on why you think it’s not? For instance, I think I would agree with you if f was an internal function used in some performance-critic part of the code. But (and maybe it’s a useful piece of context indeed:) this is a public API. If I had to describe the design in a nutshell: I don’t want to state unnecessary constraints on my function signature.
All I’m needing here is that collection yields items in an iterable fashion, i.e.for i in collection should not error. As a consequence, I would be unsatisfied to force my callers into using a Vector rather than anything they have at hand like a Tuple, a Set or a lazy iterator.
This being said, and since performance is not a concern, I would agree that converting the received argument to some standard collection like Vector could be useful indeed:
f(collection) = f(Vector(collection))
f(collection::Vector{<:Integer}) = # implement for integers
f(collection::Vector{<:AbstractString}) = # implement for strings
f(::Vector) = throw(ArgumentError("Invalid element type.."))
But I find this somewhat unsatisfying because Vector is only introduced/allocated here for dispatching purpose. Dispatching should not influence implementation this way, right?
This is also what I think. if eltype(collection) <: Integer is a good way to go if I’m not willing to include the third-party @trait macro here.
Oh, I didn’t know about those This is very close to the perfect solution because it’s semantically accurate and only uses native Julia concepts. Although it’s a little bit complicated for a small function with only a few expected possible element types, I would definitely go for it if the list of expected possible types needs to be large, or open. Maybe I need to read that part of the docs now?
Yes, this is a good solution and probably rather close to the code that one of the trait libraries would generate.
In any case: What exactly are you trying to do, i.e., why would your function need to behave differently for the element type yet needs access to the whole iterable?
Just asking, because some higher-order function might already do the trick. You already suggested that map(do_scalar, collection) would not be enough. There is also reduce though, which is probably the most general type of processing you can do when the actual collection type is unknown, i.e., you can only rely on iterating over it:
reduce(combine, collection; init=initial)
# roughly equivalent to
begin
res = initial
for ele in collection
res = combine(res, ele)
end
res
end
Yep. I think eltype(collection) can decrease performance in critical parts. Also all such types: Tuple{String}, Tuple{String, String} is a different types and you will have different methods for all of them and in some cases it can lead to long compile time.
Also Base.IteratorEltype(1) == Base.HasEltype() and it can be critical in some ways, for example if you want return vector for collection and single element when provide Int.