Associativity of promote_type

I found this via a bug report in JuMP: Error vcat(::NonlinearExpr, ::VariableRef, ::Float64) · Issue #3671 · jump-dev/JuMP.jl · GitHub.

promote_type has an implicit assumption of associativity, i.e.:

promote_type(A, promote_type(B, C)) == promote_type(promote_type(A, B), C)

That’s because the implementation is essentially promote_type(A, args...) = promote_type(A, promote_type(args...)).

This causes issues:

julia> struct Foo; x::Int end

julia> struct Bar; x::Int end

julia> Base.convert(::Type{Foo}, b::Bar) = Foo(b.x)

julia> Base.convert(::Type{Bar}, x::Int) = Bar(x)

julia> Base.promote_rule(::Type{Foo}, ::Type{Bar}) = Foo

julia> Base.promote_rule(::Type{Bar}, ::Type{Int}) = Bar

julia> promote_type(Foo, Bar, Int)
Foo

julia> promote_type(Foo, Int, Bar)
Foo

julia> promote_type(Bar, Foo, Int)
Any

julia> promote_type(Bar, Int, Foo)
Any

julia> promote_type(Int, Foo, Bar)
Any

julia> promote_type(Int, Foo, Bar)
Any

julia> promote_type(Int, Bar, Foo)
Any

Depending on the order of arguments, the type might be Foo or Any.

If it is Foo, then we are missing a direct convert method convert(::Type{Foo}, ::Int)

julia> [3, Foo(1), Bar(2)]
3-element Vector{Any}:
3
Foo(1)
Bar(2)

julia> [Foo(1), Bar(2), 3]
ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type Foo
Closest candidates are:
convert(::Type{Foo}, ::Bar)
@ Main REPL[105]:1
convert(::Type{T}, ::T) where T
@ Base Base.jl:84
Foo(::Int64)
@ Main REPL[103]:1
...
Stacktrace:
[1] setindex!(A::Vector{Foo}, x::Int64, i1::Int64)
@ Base ./array.jl:1021
[2] (::Base.var"#114#115"{Vector{Foo}})(i::Int64, v::Int64)
@ Base ./array.jl:456
[3] afoldl(::Base.var"#114#115"{Vector{Foo}}, ::Int64, ::Foo, ::Bar, ::Int64)
@ Base ./operators.jl:546
[4] getindex(::Type{Foo}, ::Foo, ::Bar, ::Int64)
@ Base ./array.jl:455
[5] vect(::Foo, ::Vararg{Any})
@ Base ./array.jl:187
[6] top-level scope
@ REPL[115]:1

Questions:

  • Am I a fool for not defining the necessary promote_rule and converts needed for associativity? I didn’t really want Int to be promoted to Foo! I wanted the Any. (But I do want Int to be promoted to Bar when needed.)

or in other words:

  • Is it expected behavior? I assume so.
  • How can one ensure that any user-defined promote_rules satisfy associativity? Just manual inspection + tests?
4 Likes