=
, which can be used for either purposes. <function>
or <operator>
).
This is just notation, and the symbols <
and >
should not be misconstrued as Julia's syntax.
Action | Keyboard Shortcut |
---|---|
Previous Section | Ctrl + 🠘 |
Next Section | Ctrl + 🠚 |
List of Sections | Ctrl + z |
List of Subsections | Ctrl + x |
Close Any Popped Up Window (like this one) | Esc |
Open All Codes and Outputs in a Post | Alt + 🠙 |
Close All Codes and Outputs in a Post | Alt + 🠛 |
Unit | Acronym | Measure in Seconds |
---|---|---|
Seconds | s | 1 |
Milliseconds | ms | 10-3 |
Microseconds | μs | 10-6 |
Nanoseconds | ns | 10-9 |
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.
=
, which can be used for either purposes. 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
.
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
x
4-element Vector{Int64}:
1
2
30
40
x = [1, 2, 3, 4]
x[x .≥ 3] = x[x .≥ 3] .* 10
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)]
x
4-element Vector{Int64}:
1
2
30
40
x = [1, 2, 3, 4]
x[3:end] = [30, 40]
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)]
x
4-element Vector{Int64}:
1
2
30
40
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
x
4-element Vector{Int64}:
10
20
30
40
x = [1, 2, 3, 4]
x[:] = x .* 10
x
4-element Vector{Int64}:
10
20
30
40
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
x
4-element Vector{Int64}:
0
0
3
4
x = [-1, -2, 3, 4]
x[x .< 0] = zeros(length(x[x .< 0]))
x
4-element Vector{Int64}:
0
0
3
4
x = [-1, -2, 3, 4]
x[x .< 0] .= zeros(length(x[x .< 0])) # identical output
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.
x = [-1, -2, 1, 2]
y = view(x, x .< 0)
y .= 0
y
2-element view(::Vector{Int64}, [1, 2]) with eltype Int64:
0
0
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`
y
2-element Vector{Int64}:
0
0
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
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'
x
4-element Vector{Int64}:
1
2
3
4
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
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
x
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
x
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
x