# Type-unstable application of \`map\`

**URL:** <https://discourse.julialang.org/t/type-unstable-application-of-map/102771>\
**Category:** Performance\
**Created:** [August 13, 2023, 4:09pm UTC](https://discourse.julialang.org/t/type-unstable-application-of-map/102771 "2023-08-13T16:09:32Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![ffevotte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ffevotte/32/6587_2.png) [@ffevotte](https://discourse.julialang.org/u/ffevotte)\
**Post date:** [August 13, 2023, 4:09pm UTC](https://discourse.julialang.org/t/type-unstable-application-of-map/102771/1 "2023-08-13T16:09:32Z")

</div>

Consider the following (minimal?) example:

```julia
julia> struct A{N}
           A(::NTuple{N,Int}) where {N} = new{N}()
       end

julia> t = (1,2)
(1, 2)

julia> T = typeof(t)
Tuple{Int64, Int64}

julia> using Test

julia> @inferred A(t) # A itself is type-stable and correctly inferred
A{2}()

julia> @inferred map(A, [t]) # ...but `map`ping A to a vector of T is not!
ERROR: return type Vector{A{2}} does not match inferred return type Union{Vector{Any}, Vector{A{2}}}

julia> map(A, T[]) # indeed, on an empty vector it returns Any[]
Any[]

```

I’m surprized by this behavior, because I think this is the first time I observe a type instability related to whether `map` is applied to an empty vector (in contrast to `reduce`, for example, where such instabilities frequently need to be fixed using a correctly typed initial value). Consider for example the following example, very similar to the above but which infers correctly:

```julia
julia> struct B
           B(::NTuple{N,Int}) where {N} = new()
       end

julia> @inferred map(B, [t])
1-element Vector{B}:
 B()

julia> map(B, T[])
B[]

```

Do you know why type instabilities appear with the parametric type `A` but not the simpler type `B`? And perhaps how to avoid them (short of type-asserting the output of `map(A, ...)`?

  

PS: if that matters, in my real use-case I can guarantee that `map(A,...)` is applied to a non-empty vector.

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [August 13, 2023, 4:28pm UTC](https://discourse.julialang.org/t/type-unstable-application-of-map/102771/2 "2023-08-13T16:28:04Z")

</div>

I’d classify this as a bug. The function `_collect` that’s ultimately being called here tries to infer the default eltype by doing `@default_eltype(iter)`. `iter` here is a `Base.Generator`, mapping `A`. The macro does the following check:

```julia
julia> @macroexpand Base.@default_eltype(iter)
quote
    #= array.jl:804 =#
    if iter isa Base.Generator && iter.f isa Base.Type
        #= array.jl:805 =#
        var"#3#T" = iter.f
    else
        #= array.jl:807 =#
        var"#3#T" = (Base.Core).Compiler.return_type(Base._iterator_upper_bound, Base.Tuple{Base.typeof(iter)})
    end
    #= array.jl:809 =#
    Base.promote_typejoin_union(var"#3#T")
end

```

So it checks whether `iter isa Base.Generator` (it is, because that’s how our `map` works), and then it checks whether `iter.f` (the function being mapped) is a type - which is then just used as the direct type for this, resulting in the later mapping only ever assuming an `A`, and not a `A{2}`.

If you use an outer constructor (thereby not passing a `Type`), it works as expected:

```julia
julia> a(t) = A(t)
a (generic function with 1 method)

julia> @inferred map(a, [t])
1-element Vector{A{2}}:
 A{2}()

```

because the `iter.f isa Base.Type` check is `false`.

This is a bit hard to find from `@code_warntype` or Cthulhu.jl alone, because of [Color incompletely specified type parameters in `@code_warntype` differently · Issue #41251 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/issues/41251)… I was only lucky to notice that the inferred types everywhere was `Type{A}`, instead of `Type{A{2}}`.

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [August 13, 2023, 4:46pm UTC](https://discourse.julialang.org/t/type-unstable-application-of-map/102771/3 "2023-08-13T16:46:47Z")

</div>

If it would hit the other path, it would infer just fine:

```julia
julia> (Base.Core).Compiler.return_type(Base._iterator_upper_bound, Base.Tuple{Base.Generator{Vector{Tuple{Int,Int}}, Type{A}}})
A{2}

```

So it’d probably be a quick & easy fix to modify `@default_eltype` to do

```julia
quote
    #= array.jl:804 =#
    if iter isa Base.Generator && iter.f isa Base.Type && !(iter.f isa Base.UnionAll)
        #= array.jl:805 =#
        var"#3#T" = iter.f
    else
        #= array.jl:807 =#
        var"#3#T" = (Base.Core).Compiler.return_type(Base._iterator_upper_bound, Base.Tuple{Base.typeof(iter)})
    end
    #= array.jl:809 =#
    Base.promote_typejoin_union(var"#3#T")
end

```

instead. Whether that is desirable though, I can’t say - there may be some other inference or compile time concerns at play here too.

---

<div class="post-metadata">

**Author:** ![ffevotte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ffevotte/32/6587_2.png) [@ffevotte](https://discourse.julialang.org/u/ffevotte)\
**Post date:** [August 13, 2023, 4:46pm UTC](https://discourse.julialang.org/t/type-unstable-application-of-map/102771/4 "2023-08-13T16:46:50Z")

</div>

Incredible! Thanks a lot!

> [@Sukera](#):
>
> This is a bit hard to find from `@code_warntype` or Cthulhu.jl alone, because of [Color incompletely specified type parameters in `@code_warntype` differently · Issue #41251 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/issues/41251)… I was only lucky to notice that the inferred types everywhere was `Type{A}`, instead of `Type{A{2}}`.

I have to admit I stared a long time at the Cthulhu output, but did not manage to figure out where instabilities were introduced.

---

<div class="post-metadata">

**Author:** ![ffevotte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ffevotte/32/6587_2.png) [@ffevotte](https://discourse.julialang.org/u/ffevotte)\
**Post date:** [August 13, 2023, 8:17pm UTC](https://discourse.julialang.org/t/type-unstable-application-of-map/102771/5 "2023-08-13T20:17:34Z")

</div>

> [@Sukera](#):
>
> I’d classify this as a bug.

Although there is an easy workaround, I submitted an issue in case it would be more beneficial to the project to fix this once and for all

> <https://github.com/JuliaLang/julia/issues/50903>
>
> Hi,
> 
> I'm filing this issue following a \[discourse thread\](https://discourse.ju…lialang.org/t/type-unstable-application-of-map/102771), in case it actually is a bug. Please feel free to close it if this behavior is actually intended.
> 
> Consider the following (minimal?) example, demonstrating a type-instable application of map to the constructor of a parametric type:
> \`\`\`julia
> julia\> struct A{N}
> A(::NTuple{N,Int}) where {N} = new{N}()
> end
> 
> julia\> t = (1,2)
> (1, 2)
> 
> julia\> T = typeof(t)
> Tuple{Int64, Int64}
> 
> julia\> using Test
> 
> julia\> @inferred A(t) # the constructor is type-stable and correctly inferred
> A{2}()
> 
> julia\> @inferred map(A, \[t\]) # ... but map is not type-stable
> ERROR: return type Vector{A{2}} does not match inferred return type Union{Vector{Any}, Vector{A{2}}}
> 
> julia\> map(A, T\[\]) # indeed, on an empty vector it returns Any\[\]
> Any\[\]
> \`\`\`
> This behavior can be observed with Julia v1.9 as well as v1.10(beta). 
> 
> \<br/\>
> 
> As uncovered by @Seelengrab in the aforementioned discourse thread, an easy workaround consists in applying \`map\` to a standard function instead of a constructor, e.g.:
> \`\`\`julia
> julia\> @inferred map(x-\>A(x), T\[\])
> A{2}\[\]
> \`\`\`
> 
> I'm paraphrasing / summarizing below what they uncovered while trying to understand this; the discourse thread contains much more details.
> 
> It seems that this issue arises in \`Base.@default\_eltype\`: because the applied function (\`A\`) is a type, the element type of the resulting array is determined to be \`A\` itself, even though it is a union-all type and it would be possible to infer the more precise element type (\`A{2}\` in this case). A possible fix would therefore consist in handling union-all types like regular functions, rather than special-casing them like the "regular" types. But doing this might have unintended consequences, so I'm not sure what the best way forward would be.
