Why are the entries identical in `zeros(JuMP.AffExpr, N)`

My understanding about the zeros function in julia is that it has independent entries.
But today I find a weird behavior which I cannot explain why.
Compare the following unexpected outcome 🔴 and the expected outcome

julia> vec = zeros(Int, 3)
3-element Vector{Int64}:
 0
 0
 0

julia> for i = 1:3
           vec[i] += i
       end

julia> vec
3-element Vector{Int64}:
 1
 2
 3

julia> using JuMP

julia> model = Model();

julia> vec = zeros(JuMP.AffExpr, 3)
3-element Vector{AffExpr}:
 0
 0
 0

julia> for i = 1:3
           x = JuMP.@variable(model)
           println("i = $i, x = $x")
           add_to_expression!(vec[i], x)
       end
i = 1, x = _[1]
i = 2, x = _[2]
i = 3, x = _[3]

julia> vec # 🔴
3-element Vector{AffExpr}:
 _[1] + _[2] + _[3]
 _[1] + _[2] + _[3]
 _[1] + _[2] + _[3]

julia> model = Model();

julia> vec = [JuMP.AffExpr(0) for _ = 1:3]
3-element Vector{AffExpr}:
 0
 0
 0

julia> for i = 1:3
           x = JuMP.@variable(model)
           println("i = $i, x = $x")
           add_to_expression!(vec[i], x)
       end
i = 1, x = _[1]
i = 2, x = _[2]
i = 3, x = _[3]

julia> vec # ✅
3-element Vector{AffExpr}:
 _[1]
 _[2]
 _[3]

They behave differently because of how JuMP’s AffExpr objects are created and stored in the array.

vec = zeros(JuMP.AffExpr, 3)

This line creates an array of three references to the same default-constructed AffExpr() object.

So when you do:

add_to_expression!(vec[i], x)

you’re modifying the same object every time. The result is that all three elements in vec appear identical and contain all the variables added across iterations.

vec = [JuMP.AffExpr(0) for _ = 1:3]

This explicitly creates three distinct AffExpr objects initialized with zero. Now:

add_to_expression!(vec3[i], x)

modifies a unique object each time, so each element in vec holds a different variable — as you intended.

Avoid zeros(JuMP.AffExpr, n) — use list comprehensions to ensure unique instances:

vec = [JuMP.AffExpr(0) for _ in 1:n]

This pattern is common for JuMP modeling when dealing with arrays of expressions.

1 Like

Why is the behavior defined to be this?
I thought this would pertain to fill.

Otherwise why we need to devise the function zeros and ones? They could have been overshadowed by fill.

julia> model = JuMP.Model();

julia> vec = fill(JuMP.AffExpr(0), 3)
3-element Vector{AffExpr}:
 0
 0
 0

julia> x = JuMP.@variable(model)
_[1]

julia> JuMP.add_to_expression!(vec[2], x)
_[1]

julia> vec
3-element Vector{AffExpr}:
 _[1]
 _[1]
 _[1]

Yeah, it’s counterintuitive.

Short answer is * zeros(T, n) → calls fill(zero(T), n)

  • For mutable types, this means all elements are the same object
  • For immutable types, this is safe and expected
  • fill(f(), n) reuses the result of f() once — does not call f() n times
  • Therefore, if you need distinct mutable objects, use a comprehension:

[AffExpr(0) for _ in 1:n]
1 Like

This is documented at

2 Likes

Although the present behavior of zeros is confusing, I’ll remember this.

We do not this redundant function in julia (I think), we suffice to have

julia> fill(rand(), 3)
3-element Vector{Float64}:
 0.7694423098899204
 0.7694423098899204
 0.7694423098899204

julia> [rand() for _ = 1:3]
3-element Vector{Float64}:
 0.5962687295988872
 0.46835512943912494
 0.30210548834623685

Yes, this is a Julia problem. It is not specific to JuMP. It applies to all types.

I think the docs should mention [AffExpr(0) for _ in 1:n], that’s much more convenient than the method explained there.

PRs accepted

PR submitted.

1 Like