allCode.jl. They've been tested under Julia 1.11.3.allCode.jl. They've been tested under Julia 1.11.3.This section continues exploring approaches to mutating vectors. The emphasis is now on in-place functions, defined as functions that mutate at least one of their arguments.
Many built-in functions in Julia have an in-place counterpart, which can easily recognized by the ! suffix in their names. These versions store the output in one of the function arguments, thereby avoiding the creation of a new object. In practice, it means that variables can be updated immediately after executing the function. For example, given a vector x, the call sort(x) produces a new vector with ordered elements, but without altering the original x. In contrast, the in-place version sort!(x) overwrites the content of x.
The benefits of in-place functions will become evident in Part II, when discussing high-performance computing. Essentially, by reusing existing objects, in-place functions eliminate the overhead associated with creating new objects.
In-place functions, also known as mutating functions, are characterized by their ability to modify at least one of their arguments. For example, given a vector x, the following function foo(x) constitutes an example of in-place function, as it modifies the content of x.
y = [0,0]
function foo(x)
x[1] = 1
endy2-element Vector{Int64}:
0
0foo(y) #it mutates 'y'y2-element Vector{Int64}:
1
0global keyword. However, its use is typically discouraged, explaining why we won't cover it.The following code illustrates this behavior by redefining a function argument and a global variable. The output reflects that foo in each example treats the redefined x as a new local variable, only existing within foo's scope.
x = 2
function foo(x)
x = 3
endx2foo(x)x #functions can't redefine global variables, only mutate them2x = [1,2]
function foo()
x = [0,0]
endx2-element Vector{Int64}:
1
2foo()x #functions can't redefine variables globally, only mutate them2-element Vector{Int64}:
1
2In Julia, many built-in functions that take vectors as arguments are available in two forms: a "standard" version and an in-place version. To distinguish between them, Julia's developers follow a convention that any function ending with ! corresponds to an in-place function.
! to a function doesn't change the function's behavior. It's simply a convention adopted by Julia's developers to emphasize the mutable nature of the operation. Its purpose is to alert users about the potential side effects of the function, thus preventing unintended modifications of objects.To illustrate these forms, let's start considering single-argument functions. In particular, we'll focus on sort. This arranges the elements of a vector in ascending order, with the option rev=true implementing a descending order. In its standard form, sort(x) creates a new vector containing the sorted elements, leaving the original vector x unchanged. In contrast, the in-place version sort!(x) directly updates the original vector x, overwriting its values with the sorted result.
x = [2, 1, 3]
output = sort(x)x3-element Vector{Int64}:
2
1
3output3-element Vector{Int64}:
1
2
3x = [2, 1, 3]
sort!(x)x3-element Vector{Int64}:
1
2
3Regarding multiple-argument functions, it's common to include an argument whose sole purpose is to store outputs. For instance, given a function foo and a vector x, the built-in function map(foo, x) has an in-place version map!(foo, <output vector>, x).
x = [1, 2, 3]
output = map(a -> a^2, x)x3-element Vector{Int64}:
1
2
3output3-element Vector{Int64}:
1
4
9x = [1, 2, 3]
output = similar(x) # we initialize `output`
map!(a -> a^2, output, x) # we update `output`x3-element Vector{Int64}:
1
2
3output3-element Vector{Int64}:
1
4
9In Julia, for-loops should always be wrapped in functions. This not only prevents issues with variable scope, but is also key for performance, as we'll discuss in Part II.
Considering this, the ability of functions to mutate their arguments is crucial. It determines that we can initialize vectors with undef values, pass them to a function, and fill them through a function via a for-loop. The examples below illustrate this application.
x = [3,4,5]
function foo!(x)
for i in 1:2
x[i] = 0
end
endfoo!(x)x3-element Vector{Int64}:
0
0
5x = Vector{Int64}(undef, 3) # initialize a vector with 3 elements
function foo!(x)
for i in eachindex(x)
x[i] = 0
end
endfoo!(x)x3-element Vector{Int64}:
0
0
0