Type inference for map and broadcast differs on empty vectors

Both map and broadcast can infer the element type if a function is applied to an empty vector, at least in many circumstances:

julia> f(x) = 3*x
julia> map(f, Int[])
Int64[]
julia> f.(Int[])
Int64[]

However, if used with constructors, broadcast does a better job than map:

julia> map(Some, Int[])
Any[]
julia> Some.(Int[])
Some{Int64}[]

Is this intended, or is it a bug? (Tried on Julia 1.9.0-rc2)

2 Likes

It does this on v1.8.5 too, and it used to infer Some on v1.4.1 (but not Some{Int}).

  1. It seems the difference starts at _collect(c, itr, ::EltypeUnknown, isz::Union{HasLength,HasShape}) in array.jl.
  2. The _similar_for calls are relevant to empty vectors, and the method signatures are different too.
  3. But the root difference seems to be Base.@default_eltype(Base.Generator(Some,Int[])), which is Any on v1.8.5 but Some on v1.4.1. It used to return Some by accessing the .f field of the Generator, but now it computes Base.promote_typejoin_union(Some) to Any.
  4. promote_typejoin_union(::Type{T}) where T in promotion.jl has many branches but there’s 2 of note there. If T isa DataType, it just returns T, but if T isa UnionAll, it returns Any. Some is not a concrete type, but an iterated union Some{T} where T. I’m not sure why it was decided to remove that type information instead of returning T, but it actually is commented # TODO: compute more precise bounds so that may change eventually.

Aside: I have no idea what Some is for, I read the docs before and didn’t know why there needed to be a distinction between nothing and Some{nothing}. It doesn’t seem necessary for Base.something to find the first argument that isn’t nothing.

I had created an issue about this sometime back:

So the problem is the following: If we say

@default_eltype(Base.Generator(Some, Int[]))

then (code taken from array.jl)

macro default_eltype(itr)
    I = esc(itr)
    return quote
        if $I isa Generator && ($I).f isa Type
            T = ($I).f
        else
            T = Core.Compiler.return_type(_iterator_upper_bound, Tuple{typeof($I)})
        end
        promote_typejoin_union(T)
    end
end

we end up in the if branch. This gives Some , which is later promoted to Any. Wouldn’t the problem disappear if ($I).f isa Type were replaced by isconcretetype(($I).f)?

(For concrete types this would still use the assumption that a constructor for a type T returns an element of type T. Maybe this could be fixed by removing the if branch completely.)