pic
Personal
Website

5g. In-Place Assignments

PhD in Economics

Introduction

This section explores approaches for updating the values of a vector, a technique commonly known as in-place assignments. The method involves replacing existing elements with new values, giving rise to the expression " modifying values in place."

The benefits of in-place assignments will become clear in Part II, when we analyze performance. Basically, they enable the reuse of the same vector, thus eliminating the need for memory allocation to create a new object.

In each of the following sections, we'll present an approach to mutating vectors.

Warning!
Although called "in-place assignments", the concept is related to mutations rather than assignments. The name simply reflects that the technique involves the assignment operator =, which can be used for either purposes.
Remark
Before proceeding, it's essential that you review the definitions of slices introduced in the previous section. Recall that, given a vector x, a slice refers to a subset of x's elements, created through the expression x[<indices>]. Moreover, a slice can act as a copy or a view. This depends on whether a new object with its own memory address is created or the slice references the original memory address of x.

Mutation Via Vectors

The most straightforward way to mutate a vector is by replacing a slice with another vector. This can be achieved via statements of the form x[<indices>] = <expression>, where <expression> must match the length of x[<indices>]. [note] When the slice comprises a single element, a scalar can be used directly instead of a vector. The method relies on that a slice on the left-hand side of = behaves as a view, thus referencing to the original object rather than to a new one.

The examples below demonstrate the flexibility of the approach to reference vectors in <expression>. It also allows that the original slice can be reused, which is a common technique for defining the replacing object. Furthermore, the last two examples show that any indexing method can be applied to define x[<indices>].

The examples adopt LHS and RHS as shorthand for left-hand side and right-hand side of =, so that LHS refers to <expression> and RHS denotes x[<indices>].

x         = [1, 2, 3, 4]

x[3]      = 30
x[4]      = 40
Output in REPL
julia>
x
4-element Vector{Int64}:
  1
  2
 30
 40
x         = [1, 2, 3, 4]


x[x .≥ 3] = x[x .≥ 3] .* 10
Output in REPL
julia>
x
4-element Vector{Int64}:
  1
  2
 30
 40
x         = [1, 2, 3, 4]


x[3:end]  = [x[i] * 10 for i in 3:length(x)]
Output in REPL
julia>
x
4-element Vector{Int64}:
  1
  2
 30
 40
x         = [1, 2, 3, 4]


x[3:end]  = [30, 40]
Output in REPL
julia>
x
4-element Vector{Int64}:
  1
  2
 30
 40
x         = [1, 2, 3, 4]


x[x .≥ 3] = [x[i] * 10 for i in 3:length(x)]
Output in REPL
julia>
x
4-element Vector{Int64}:
  1
  2
 30
 40

Remark
When it comes to modifying all elements of a vector x, it's essential to distinguish between mutating their values and reassignments of x. Although these two operations yield the same result, they're implemented differently internally. Specifically, the former modifies the existing vector in place, reusing the original memory address associated with x, while reassignments create a new object and therefore a new memory address.

x    = [1, 2, 3, 4]

x    = x .* 10
Output in REPL
julia>
x
4-element Vector{Int64}:
 10
 20
 30
 40
x    = [1, 2, 3, 4]

x[:] = x .* 10
Output in REPL
julia>
x
4-element Vector{Int64}:
 10
 20
 30
 40

Mutation Via Broadcasting "="

When the goal is to replace each element of a slice with the same scalar value, the previous method can result in excessive boilerplate code. This follows because the technique requires a vector that matches the number of elements to be replaced.

To address this, we introduce a more convenient approach, based on broadcasting the assignment operator =. Its syntax is x[<indices>] .= <expression>, whose key advantage is that <expression> can be either a vector or a scalar. This feature allows for substituting each element of x[indices] with a scalar <number> by a statement as simple as x[indices] .= <number>.

In the following, we illustrate the approach by replacing each negative value in a vector x with zero. For comparison, we also present an implementation based on the previous section's method.

x          = [-1, -2, 3, 4]

x[x .< 0] .= 0
Output in REPL
julia>
x
4-element Vector{Int64}:
 0
 0
 3
 4
x          = [-1, -2, 3, 4]

x[x .< 0]  = zeros(length(x[x .< 0]))
Output in REPL
julia>
x
4-element Vector{Int64}:
 0
 0
 3
 4
x          = [-1, -2, 3, 4]

x[x .< 0] .= zeros(length(x[x .< 0]))           # identical output
Output in REPL
julia>
x
4-element Vector{Int64}:
 0
 0
 3
 4

Another advantage of this approach is its capability for mutating slices defined through the view function. This provides a more concise notation and allows for splitting mutations into multiple manageable steps.

Another application of .= arises when we need to perform various operations based on the same slice. Nonetheless, repeatedly referencing the same slice is error-prone and could quickly become tedious. In such cases, a more convenient approach is to create a slice's view and then operate with it.

Wronly Applying The Approach

x  = [-1, -2, 1, 2]

y  = view(x, x .< 0)
y .= 0
Output in REPL
julia>
y
2-element view(::Vector{Int64}, [1, 2]) with eltype Int64:
 0
 0

julia>
x
4-element Vector{Int64}:
 0
 0
 1
 2
x  = [-1, -2, 1, 2]

y  = x[x .< 0]              # `y` is a new object
y .= 0                      # this does NOT modify `x`
Output in REPL
julia>
y
2-element Vector{Int64}:
 0
 0

julia>
x
4-element Vector{Int64}:
 -1
 -2
  1
  2

Notice that you need to create a view for this approach to work. Otherwise, you'll be creating a new copy of the object y, and hence updating the values of y without any impact on x. Likewise, you can create a view and then replace its values with those from another vector.

x  = [1, 2, 3, 4]

y  = view(x, x .≥ 3)
y .= x[x .≥ 3] .* 10
Output in REPL
julia>
x
4-element Vector{Int64}:
  1
  2
 30
 40
x  = [1, 2, 3, 4]

y  = view(x, x .≥ 3)
y  = x[x .≥ 3] .* 10    # this creates a new variable 'y'
Output in REPL
julia>
x
4-element Vector{Int64}:
 1
 2
 3
 4

Mutation Via For-Loops

In-place assignments can also be achieved via a for-loop. This option emerges naturally when the goal is to populate vectors with specific values. The process requires initializing a vector, which we then fill with the desired values.

Next, we provide a simple illustration of the approach. A more thorough explanation is relegated to the next section, where we study functions that mutate objects. The reason is that for-loops should always be wrapped in functions, as failing to do so can severely affect performance.

x = Vector{Int64}(undef, 3)  # `x` is initialized with 3 undefined elements


for i in eachindex(x)
    x[i] = i
end
Output in REPL
julia>
x
y = [3, 4, 5]
x = similar(y)            # `x` mimicks the type of `y`, which is Vector{Int64}(undef, 3)

for i in eachindex(x)
    x[i] = i
end
Output in REPL
julia>
x

Remark
Vectors initialized with a specific type can only be mutated by objects sharing the same type. For instance, we would've obtained an error above if the values replaced had a type different from Int64.

x = Vector{Int64}(undef, 3)  # `x` is initialized with 3 undefined Int64 elements


for i in eachindex(x)
    x[i] = i * 2.5                
end

Output in REPL
julia>
x
ERROR: InexactError: Int64(2.5)

y = [3, 4, 5]
x = similar(y)            # `x` has the same type as `y`, which is Vector{Int64}(undef, 3)

for i in eachindex(x)
    x[i] = i * 2.5
end

Output in REPL
julia>
x
ERROR: InexactError: Int64(2.5)