# Associativity of promote\_type

**URL:** <https://discourse.julialang.org/t/associativity-of-promote-type/109958>\
**Category:** Internals & Design\
**Created:** [February 8, 2024, 10:27pm UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958 "2024-02-08T22:27:49Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![odow](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/odow/32/28685_2.png) [@odow](https://discourse.julialang.org/u/odow)\
**Post date:** [February 8, 2024, 10:27pm UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958/1 "2024-02-08T22:27:49Z")

</div>

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

`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](https://github.com/JuliaLang/julia/blob/aba8afc95ac647af941342d084f43e6f19abe3e6/base/promotion.jl#L302) is essentially `promote_type(A, args...) = promote_type(A, promote_type(args...))`.

This causes issues:

```julia
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
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 `convert`s 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_rule`s satisfy associativity? Just manual inspection + tests?

---

<div class="post-metadata">

**Author:** ![kimikage](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kimikage/32/14534_2.png) [@kimikage](https://discourse.julialang.org/u/kimikage)\
**Post date:** [April 5, 2024, 11:17pm UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958/2 "2024-04-05T23:17:50Z")

</div>

FYI, the implementation has changed for the nightly build since March 2024.

> <https://github.com/JuliaLang/julia/pull/53665>
>
> It is easy to accidentally call these functions (they are used by vcat, which is… syntax) with very long lists of values, causing inference to crash and take a long time. The \`afoldl\` function can handle that very well however, while naive recursion did not.
> 
> Fixes #53585

Two important things to note here are:

- We can’t try every pair because of the combinatorial explosion.
- The change in the order is not considered a so-called breaking change (for now).

---

<div class="post-metadata">

**Author:** ![benninkrs](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/benninkrs/32/3191_2.png) [@benninkrs](https://discourse.julialang.org/u/benninkrs)\
**Post date:** [April 6, 2024, 1:54pm UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958/3 "2024-04-06T13:54:26Z")

</div>

Is this a problem in practice? Your example seems a bit artificial. If one has a non-associative binary operation, it probably wouldn’t be invoked with more than 2 arguments anyway. But maybe 2-argument `promote_type` wouldn’t be sufficient for operations that inherently take more than 2 arguments.

It makes me wonder if `promote_type` should dispatch on the function as well as the argument types.

---

<div class="post-metadata">

**Author:** ![odow](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/odow/32/28685_2.png) [@odow](https://discourse.julialang.org/u/odow)\
**Post date:** [April 6, 2024, 7:45pm UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958/4 "2024-04-06T19:45:00Z")

</div>

Yes, this was a bug in JuMP. See the link in the first post. Ive since fixed by adding the missing convert method.

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [April 6, 2024, 8:29pm UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958/5 "2024-04-06T20:29:37Z")

</div>

> [@benninkrs](#):
>
> It makes me wonder if `promote_type` should dispatch on the function as well as the argument types.

There’s no need for that, because if you want to do something specific to a particular function, you can implement whatever rule you want simply as a method of that function, e.g.

```julia
myfunc(x::Number, y::Number) = x + y
myfunc(x::Float16, y::Int) = myfunc(Float64(x), y) # custom promotion rule

```

---

<div class="post-metadata">

**Author:** ![bertschi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bertschi/32/33462_2.png) [@bertschi](https://discourse.julialang.org/u/bertschi)\
**Post date:** [April 7, 2024, 8:38am UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958/6 "2024-04-07T08:38:58Z")

</div>

To be honest, I would expect `promote_type` to be order-independent, i.e., to commute, as well. Fail to see how it could be useful to have the promoted type be different due to ordering?

---

<div class="post-metadata">

**Author:** ![kimikage](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kimikage/32/14534_2.png) [@kimikage](https://discourse.julialang.org/u/kimikage)\
**Post date:** [April 7, 2024, 3:14pm UTC](https://discourse.julialang.org/t/associativity-of-promote-type/109958/7 "2024-04-07T15:14:45Z")

</div>

As I wrote above, we do so not because it is useful, but because there is no other way.  
Order dependencies can occur even if there are no obvious bugs.

```julia
julia> VERSION
v"1.6.7"

julia> [0.0f0, Complex(0,1), pi] |> typeof
Vector{ComplexF64} (alias for Array{Complex{Float64}, 1})

julia> [0.0f0, pi, Complex(0,1)] |> typeof
Vector{ComplexF64} (alias for Array{Complex{Float64}, 1})

julia> [Complex(0,1), 0.0f0, pi] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

julia> [Complex(0,1), pi, 0.0f0] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

julia> [pi, 0.0f0, Complex(0,1)] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

julia> [pi, Complex(0,1), 0.0f0] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

```

```julia
julia> VERSION
v"1.12.0-DEV.283"

julia> [0.0f0, Complex(0,1), pi] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

julia> [0.0f0, pi, Complex(0,1)] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

julia> [Complex(0,1), 0.0f0, pi] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

julia> [Complex(0,1), pi, 0.0f0] |> typeof
Vector{ComplexF64} (alias for Array{Complex{Float64}, 1})

julia> [pi, 0.0f0, Complex(0,1)] |> typeof
Vector{ComplexF32} (alias for Array{Complex{Float32}, 1})

julia> [pi, Complex(0,1), 0.0f0] |> typeof
Vector{ComplexF64} (alias for Array{Complex{Float64}, 1})

```
