There have been a few threads over the years about how to declare a function argument as “iterable” somehow, for example in Iterable type signature? .
For me (at least in my latest situation) the important part isn’t that I declare something as iterable, it’s declaring what kind of items the iteration returns. In other words, I’m fine accepting a Vector{T}, or a Base.Generator{T}, or a Tuple{T}, or an AbstractRange{T}, or whatever, but it’s important to specify what that T is.
Maybe one option is to do “casting” inside the function body?
julia> function do_stuff(a, b)
for (ai::Int, bi::Int) in zip(a, b)
println("$ai: $bi")
end
end
do_stuff (generic function with 1 method)
julia> do_stuff([1, 2], [4, 5])
1: 4
2: 5
julia> do_stuff([1, 2], [4.0, 5.0])
1: 4
2: 5
julia> do_stuff([1, 2], ["4", "5"])
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
I think that basically accepts the right inputs and raises an error otherwise, but I don’t love the type error being uncaught until execution time of that function. Any progress on declarations here that I’ve missed over the past few years, that would let me express that those arguments must return a specific type when iterated upon?
Note that you can actually catch this error at compile time:
julia> using JET
julia> function do_stuff(a, b)
for (ai::Int, bi::Int) in zip(a, b)
println("$ai: $bi")
end
end
do_stuff (generic function with 1 method)
julia> report_opt(do_stuff, (Vector{Int}, Vector{String}))
═════ 1 possible error found ═════
┌ do_stuff(a::Vector{Int64}, b::Vector{String}) @ Main ./REPL[18]:2
│ runtime dispatch detected: convert(Int, %69::String)
└────────────────────
The issue here is defining what Iterable means. You’d think that, it means hasmethod(iterate, (T,)) -> true. But alas, iterate is defined on all sorts of scalars, including Int. Also, someone could do some like
struct T end
Base.iterate(::T) = error("T is not Iterable")
Many iterables simply don’t know what type they’ll return until they actually generate a value. They definitionally won’t know if they’re ok until runtime.
Adding implicit or explicit checks can be a good solution.
It’s a counter example to the statement “The existence of the iterate method implies that an object is non-scalar/a container of elements” which I think is a reasonable definition of the word “Iterable” (even if it is not correct in the context of Julia).
It’s not a counter example though. Ints are containers of elements in julia (specifically, containers of a single element). Julia is just a little weird in that “scalar” doesn’t necessarily mean the opposite of “container of elements”.
julia> length(1)
1
julia> map(x -> 2x, 1)
2
julia> [x/2 for x in 1]
0-dimensional Array{Float64, 0}:
0.5
julia> collect(1)
0-dimensional Array{Int64, 0}:
1
NB: there’s a difference between declaring the type of a variable, and doing a typeassert on some expression. In your code example you did the former, but from your words it seems like you expect the latter. The difference is that a typeassert just asserts a type, throwing otherwise, while assigning to a variable with declared type runs convert first. Which one you want depends on what exactly you’re doing.
That’s what eltype does. However, for arbitrary iterators it should be viewed as defined on a best-effort basis, that is, eltype(iterator) might return Any even if all elements are of the same type.
The best practice is usually to just do a typeassert. Assuming you really need it.
OP makes a list of examples and does not include any subtypes of Number which indicates to me that they mean Iterable in a colloquial sense (and in the sense used in other languages). Before programming in Julia, I certainly didn’t think about the number 1 as a container of numbers that only includes 1 but has no size.
I added my parenthetical aside at the end – “even though this is not true in Julia” because I’m aware that numbers are zero-dimensional array, but that’s in the FAQ because it is surprising when you first learn it. Or, at least, it was for me!