Generic expressions are represented by <something> (e.g., <function> or <operator>).
This is just notation, and the symbols < and > should not be misconstrued as Julia's syntax.
PAGE LAYOUT
If you want to adjust the size of the font, zoom in or out on pages by respectively using Ctrl++ and Ctrl+-. The layout will adjust automatically.
LINKS TO SECTIONS
To quickly share a link to a specific section, simply hover over the title and click on it. The link will be automatically copied to your clipboard, ready to be pasted.
KEYBOARD SHORTCUTS
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 + 🠙
TIME MEASUREMENT
When benchmarking, the equivalence of time measures is as follows.
A key feature of programming is its ability to automate repetitive tasks, making for-loops play a crucial role in coding. They let you execute the same block of code repeatedly, treating each element in a list as a different input.
While for-loops are fundamental in every programming language, their importance is especially pronounced in Julia: unlike other languages (e.g., Matlab, Python, and R), which often discourage for-loops in performance-critical code, Julia relies on them to achieve high performance.
The role of for-loops in optimizing performance will be explored in Part II. Here, we’ll primarily introduce the tool itself, focusing on its syntax, constructions, and common iteration techniques.
Syntax
For-loops delimit their scope via the keywords for and end. To illustrate their syntax, consider the function println(a), which evaluates a and displays its output in the REPL. In case a is a string, println(a) simply displays the word stored in a. The following script repeatedly applies println to display each word contained in a collection.
for x in ["hello","beautiful","world"]
println(x)
end
Output in REPL
"hello"
"beautiful"
"world"
Remark
The keyword in can be replaced by ∈ or =. [note] Recall that ∈ can be written through tab completion using the command \in. Consequently, the following constructions are all equivalent.
for x in ["hello","beautiful","world"]
println(x)
end
for x ∈ ["hello","beautiful","world"]
println(x)
end
for x = ["hello","beautiful","world"]
println(x)
end
Furthermore, we can employ any character or term to describe the iteration variable. For instance, we iterate below using word.
for word in ["hello","beautiful","world"]
println(word)
end
Output in REPL
"hello"
"beautiful"
"world"
Based on this example, we can identify three components that characterize a for-loop:
A code block to be executed: represented in the example by println(x).
A list of elements: represented in the example by ["hello","beautiful","world"]. This specifies the elements over which we'll apply the code block. The list can contain elements with any data type (e.g., strings, numbers, and even functions). The only requirement is that the list must be an iterable object, defined as a collection whose elements can be accessed individually. An examples of iterable object is vectors, as in the example. However, we'll also introduce others most commonly used, such as ranges.
An iteration variable: represented in the example by x. This serves as a label that takes on the value of each element in the list, one at a time, during each iteration. The iteration variable is a local variable, with no significance outside the for-loop. Its sole purpose is to provide a convenient way to access and manipulate the elements of the list within the loop.
In the following sections, we'll explore different objects that can serve as lists. Furthermore, we'll show that these lists can comprise elements not immediately obvious. A typical example is functions, making it possible to apply different functions to the same object.
Always Wrap For-Loops in Functions
At this stage of the website, we're still introducing fundamental concepts. Thus, we're presenting subjects in their simplest form for learning purposes. In particular, this explains why for-loops will be written in the global scope.
However, you should always wrap for-loops in functions. Executing for-loops outside a function severely degrades performance, and is additionally subject to different rules regarding variable scoping. [note] In fact, older versions of Julia were restricting the use of for-loops in the global scope.
Iterating over Indices
So far, we've considered a simple list like ["hello", "beautiful", "world"] to demonstrate how for-loops work. In real applications, however, manually specifying each element in a list is impractical. Fortunately, when a list follows a predictable pattern (e.g., a sequence of numbers), we can simply describe the pattern that generates those elements.
Building on this insight, we'll next explore how to define ranges. They let users define a sequence of numbers, which is particularly useful to access elements of a collection through their indices.
Ranges
Ranges in Julia are defined via the syntax <begin>:<steps>:<end>, where <begin> represents the starting index and <end> the ending index. Likewise, <steps> sets the increment between values, defaulting to one when the term is omitted. We can also reverse the order of the sequence, by providing a negative value for <steps>. All this is demonstrated below.
for i in 1:2:5
println(i)
end
Output in REPL
1
3
5
for i in 3:-1:1
println(i)
end
Output in REPL
3
2
1
Remark
The application of ranges isn't limited to for-loops. They can also define vectors when used in combination with the collect function.
x = collect(4:6)
Output in REPL
julia>
x
3-element Vector{Int64}:
4
5
6
Iterating over Indices of an Array
Ranges can be employed to access elements of a collection. When combined with a for-loop, it makes it possible to apply the same code block to each element of a vector.
Specifically, the expression 1:length(x), where length(x) returns the number of elements in x, allows iteration over all indices of a vector x. The same functionality can be achieved with the function eachindex(x). In fact, this is the recommended approach for iterating over all elements, as it returns an iterator optimized for each iterable object.
x = [4, 6, 8]
for i in 1:length(x)
println(x[i])
end
Output in REPL
4
6
8
x = [4, 6, 8]
for i in eachindex(x)
println(x[i])
end
Output in REPL
4
6
8
Remark
There are other approaches to iterating over all indices of a vector x. For instance, you can use LinearIndices(x), or firstindex(x):lastindex(x) to specify a range from the first to the last index of x.
This multiplicity of methods exists to handle non-standard indices, such as those provided by the OffsetArrays.jl package. This package sets the first index of arrays to 0, a common convention in many programming languages. Nevertheless, unless you're developing a package for other users, you don't need to worry about which approach to implement. Indeed, they can all be used interchangeably, as shown below.
x = [4, 6, 8]
for i in eachindex(x)
println(x[i])
end
Output in REPL
4
6
8
x = [4, 6, 8]
for i in 1:length(x)
println(x[i])
end
Output in REPL
4
6
8
x = [4, 6, 8]
for i in LinearIndices(x)
println(x[i])
end
Output in REPL
4
6
8
x = [4, 6, 8]
for i in firstindex(x):lastindex(x)
println(x[i])
end
Output in REPL
4
6
8
Among the available alternatives, eachindex is preferable because it automatically selects the most efficient method for each type of collection. Additionally, the syntax is consistent across all indexing conventions.
Rules for Variable Scope in For-Loops
Similar to functions, for-loops create a new variable scope. In fact, the scoping rules for both are similar, with one key difference: for-loops can modify global variables, whereas functions cannot.
Warning!
The general scoping rules presented here apply universally, except in rare edge cases that result from poor coding practices. Since this scenario is uncommon, we only outline it next.
Basically, the issue occurs when i) the for-loop is not wrapped in a function, ii) a local variable shares the same name as a global variable, and iii) the script is run non-interactively (i.e., using the function include and a script file). [note] There are two methods to execute a script. The first method is what we've been using so far, where you work interactively with Julia. This includes running commands in the REPL's prompt julia>and the execution of a script through a code editor. The second method consists of executing files that store scripts through the function include.
Unless the three conditions hold simultaneously, you don't have to worry about this scenario. And even if this occurs, Julia will display a warning in the REPL indicating that there's a problem with your code.
To formalize the variable scope of for-loops, we'll refer to a variable x. The rules governing its scope are:
the variable of iteration x is always local, regardless of whether there's a variable x defined outside the for-loop.
if there's no variable named x outside the for-loop, x is a new local variable. Moreover, this variable won't be accessible outside the for-loop.
if there's a variable named x outside the for-loop, x refers to this variable.
The following code snippets illustrate the first two rules, which exclusively refer to local variables. The second example is particularly noteworthy, as it highlights a common mistake made by beginners: running a for-loop that defines a local variable, and then trying to access it outside the for-loop.
x = 2
for x in ["hello"] # this 'x' is local, not related to 'x = 2'
println(x)
end
Output in REPL
"hello"
#no `x` outside the for-loop
for word in ["hello"]
x = word # `x` is local to the for-loop, not available outside it
end
Output in REPL
julia>
x
ERROR: UndefVarError: x not defined
Likewise, the following example demonstrates the consequences of the last rule we mentioned. This refers to the consequences of variable scope for global variables.
x = [2, 4, 6]
for i in eachindex(x)
x[i] * 10 # it refers to the `x` outside of the for-loop
end
Output in REPL
julia>
x
3-element Vector{Int64}:
20
40
60
x = [2, 4, 6]
for word in ["hello"]
x = word # it reassigns the `x` defined outside the for-loop
end
Output in REPL
julia>
x
"hello"
Array Comprehensions
To seamless create arrays via for-loops, you can use array comprehensions. Their syntax is [<expression> for... if...], where <expression> denotes either an operation or a function.
For illustration purposes, consider a vector x. Suppose that the goal is to create a vector y with elements equal to the square of the corresponding element in x. The following code snippets show two approaches to creating y via array comprehensions.
x = [1,2,3]
y = [a^2 for a in x] # or y = [x[i]^2 for i in eachindex(x)]
Output in REPL
julia>
y
3-element Vector{Int64}:
1
4
9
x = [1,2,3]
foo(a) = a^2
y = [foo(a) for a in x] # or y = [foo(x[i]) for i in eachindex(x)]
Output in REPL
julia>
y
3-element Vector{Int64}:
1
4
9
Array comprehensions also allow for creating vectors based on conditions. In such instances, the condition must be placed at the end of the expression.
x = [i for i in 1:4 if i ≤ 3]
Output in REPL
julia>
x
3-element Vector{Int64}:
1
2
3
Remark
Array comprehensions can also create matrices. Its syntax demands a comma to separate the description of each dimension.
y = [i * j for i in 1:2, j in 1:2]
Output in REPL
julia>
y
2×2 Matrix{Int64}:
1 2
2 4
Iterating over Multiple Objects
Thus far, we've considered for-loops that iterate over single values. We now extend their application to simultaneous iterations over multiple values. Specifically, we'll examine two scenarios: simultaneous iterations over two lists and over both the indices and values of a vector.
Iterating over Two Lists
Depending on how elements should be combined, we can define two approaches to simultaneously iterating over two lists x and y. First, the function Iterators.product(x,y) makes it possible to iterate over all the possible combinations of elements. This function is part of the package Iterators, imported by default in each Julia session.
Alternatively, you can iterate over all the ordered pairs of x and y. This is implemented through the function zip(x,y), which provides the pair of i-th elements from x and y in the i-th iteration.
list1 = [1, 2]
list2 = [3, 4]
for (a,b) in Iterators.product(list1,list2) #it takes all possible combinations
println([a,b])
end
Output in REPL
[1,3]
[2,3]
[1,4]
[2,4]
list1 = [1, 2]
list2 = [3, 4]
for (a,b) in zip(list1,list2) #it takes pairs of elements with the same index
println([a,b])
end
Output in REPL
[1,3]
[2,4]
Using zip, we can also iterate over multiple values via array comprehensions.
x = [i * j for i in 1:2 for j in 1:2]
Output in REPL
julia>
x
4-element Vector{Int64}:
1
2
2
4
x = [i * j for (i,j) in zip(1:2, 1:2)]
Output in REPL
julia>
x
2-element Vector{Int64}:
1
4
Simultaneously Iterating over Indices and Values
To iterate over each pair of index-value of a vector, we can employ the enumerate function.
x = ["hello", "world"]
for (index,value) in enumerate(x)
println("$index $value")
end
Output in REPL
"1 hello"
"2 world"
x = [10, 20]
y = [index * value for (index,value) in enumerate(x)]
Output in REPL
julia>
y
2-element Vector{Int64}:
10
40
Iterating over Functions
Functions in Julia are first-class objects, also referred to as first-class citizens. This means that functions can be manipulated just like any other data type, such as strings and numbers. In particular, this property makes it possible to store functions in a vector and apply them sequentially to an object. The following example illustrates this by computing descriptive statistics of a vector x.
x = [10, 50, 100]
list_functions = [maximum, minimum]
descriptive(vector,list) = [foo(vector) for foo in list]