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.
Our previous discussions on collections have centered around vectors and tuples. The current section expands on the subject, offering a more comprehensive analysis and introducing two new types of collections: named tuples and dictionaries.
Specifically, we'll show how to characterize collections through keys and values, methods for their manipulation, and approaches to transforming one collection into another.
Keys and Values
Most collections in Julia are characterized by keys, which serve as unique identifiers of their elements and have a corresponding value associated to each. [note] Not all collections represent maps of keys to values. For example, the type Set, which represents a group of unique unordered elements, doesn't have keys. For instance, the vector x = [2, 4, 6] has the indices 1, 2, 3 as its keys, and 2, 4, 6 are their respective values.
Keys are more general than indices—they encompass all the possible identifiers of a collection's elements (e.g., strings, numbers, or other objects), while indices exclusively employ integers as identifiers. To know the keys and values of a collection, Julia provides the functions keys and values.
The following code snippets illustrate the use of keys and values. The examples are based on vectors and tuples, whose keys are exclusively represented by indices. Importantly, neither keys nor values necessarily return a vector by default, requiring the function collect to get a vector form.
x = [4, 5, 6]
Output in REPL
julia>
collect(keys(x))
3-element Vector{Int64}:
1
2
3
julia>
collect(values(x))
3-element Vector{Int64}:
4
5
6
x = (4, 5, 6)
Output in REPL
julia>
collect(keys(x))
3-element Vector{Int64}:
1
2
3
julia>
collect(values(x))
3-element Vector{Int64}:
4
5
6
The Type 'Pair'
Collections of key-value pairs in Julia are represented by the type Pair{<key type>, <value type>}. While we won't directly work with objects of having this type, they form the basis for constructing dictionaries and named tuples.
A key-value pair can be created by using the operator => as in <key> => <value>. For instance, "a" => 1 represents a pair, where a is its key and 1 its corresponding value. In addition, we can create pairs through the function Pair(<key>, <value>), such that Pair("a",1) is equivalent. Moreover, given a pair x, we can access its key by x[1] or x.first, and its value by x[2] or x.second. All this is demonstrated below.
some_pair = ("a" => 1) # or simply 'some_pair = "a" => 1'
some_pair = Pair("a", 1) # equivalent
Output in REPL
julia>
some_pair
"a" => 1
julia>
some_pair[1]
"a"
julia>
some_pair.first
"a"
some_pair = ("a" => 1) # or simply 'some_pair = "a" => 1'
some_pair = Pair("a", 1) # equivalent
Output in REPL
julia>
some_pair
"a" => 1
julia>
some_pair[2]
1
julia>
some_pair.second
1
The Type 'Symbol'
The type used to represent keys can vary depending on the collection. One important type used as key is Symbol, which provides an efficient way to represent string-based keys. A symbol with name x is denoted :x, and can be created from strings using the function Symbol(<some string>). [note] This type also enables programmatic creation of variables. For example, it can be employed for creating new columns in the package DataFrames, which provides a table representation of data.
Tuples and named tuples should only be used for small collections. Using them for large collections can lead to slow operations, or even stack overflows that exceed the computer's processing capacity. In such situations, arrays remain the preferred choice.
Defining what constitutes small is challenging, and unfortunately there's no definitive answer. We can only indicate that collections with fewer than 10 elements are undoubtedly small, while those exceeding 100 elements clearly violate the definition.
Named tuples share several similarities with regular tuples, including their immutability. However, they also exhibit some differences. One of them is that the keys of named tuples are objects of typeSymbol, unlike regular tuples that employ numerical indices.
Named tuples also differ syntactically, in that they must always be enclosed in parentheses ()—omitting them is not possible, unlike with regular tuples. Furthermore, when creating a named tuple with a single element, the notation requires either a trailing comma , after the element (similar to regular tuples) or a leading semicolon ; before the element. [note] The semicolon notation ; may seem odd, but it actually comes from the syntax for keyword arguments in functions.
To construct a named tuple, each element must be specified in the format <key> = <value>, such as a = 10. Alternatively, you can use a pair <key with Symbol type> => <value>, as in :a => 10. Once a named tuple nt is created, you can access its element a by using either nt[:a] or nt.a.
The following code snippets illustrate all this.
# all 'nt' are equivalent
nt = ( a=10, b=20)
nt = (; a=10, b=20)
nt = ( :a => 10, :b => 10)
nt = (; :a => 10, :b => 10)
Output in REPL
julia>
nt
(a = 10, b = 20)
julia>
nt.a
10
julia>
nt[:a] #alternative way to access 'a'
10
# all 'nt' are equivalent
nt = ( a=10,)
nt = (; a=10 )
nt = ( :a => 10,)
nt = (; :a => 10 )
#not 'nt = (a = 10)' -> this is interpreted as 'nt = a = 10'
#not 'nt = (:a => 10)' -> this is interpreted as a pair
Output in REPL
julia>
nt
(a = 10, )
julia>
nt.a
10
julia>
nt[:a] #alternative way to access 'a'
10
Remark
Note that we can also employ the functions keys and values here.
Code
nt = (a=10, b=20)
Output in REPL
julia>
collect(keys(nt))
2-element Vector{Symbol}:
:a
:b
julia>
values(nt)
(10, 20)
Distinction Between The Creation Of Tuples and Named Tuples
It's possible to create named tuples based on individual variables. For instance, given variables x = 10 and y = 20, you can define nt = (; x, y). This creates a named tuple with keys x and y, and corresponding values 10 and 20.
The semicolon ; is crucial in this construction, as it distinguishes named tuples from regular tuples. Omitting it, as in nt = (x, y), would result in a regular tuple instead.
x = 10
y = 20
nt = (; x, y)
tup = (x, y)
Output in REPL
julia>
nt
(x = 10, y = 20)
julia>
tup
(10, 20)
x = 10
nt = (; x)
tup = (x, )
Output in REPL
julia>
nt
(x = 10,)
julia>
tup
(10,)
Dictionaries
Dictionaries are collections of key-value pairs, exhibiting three distinctive features:
The keys of dictionaries can be any object: strings, numbers, or other objects (e.g., vectors, tuples) are all possible.
Dictionaries are mutable: you can modify, add, and remove elements.
Dictionaries are unordered: keys don't have any order attached.
Dictionaries are created through the function Dict, with each argument denoting key-value pairs through the notation <key> => <value>.
As regular dictionaries are unordered, accessing their elements doesn't follow any pattern. The following example illustrates this, where a vector collects the keys of a dictionary. [note] The package OrderedCollections addresses this, by offering a special dictionary called OrderedDict. It behaves pretty similarly to regular dictionaries, including its syntax.
Both dictionaries and named tuples can be constructed using other collections as their source. This is feasible as long as the source collection possesses a key-value structure. In the following, we show how several collections are employed to create dictionaries in particular.
keys_for_nt = [:a, :b]
values_for_nt = [10, 20]
vector_keys_values = [(keys_for_nt[i], values_for_nt[i]) for i in eachindex(keys_for_nt)]
nt = NamedTuple(vector_keys_values)
Previously, we've demonstrated how to create a named tuple nt using variables as its source. This approach automatically defines the key-value pairs of the named tuple by each variable's name and value.
Next, we show that we are also able to perform the opposite operation: extracting values from a named tuple and assigning them to individual variables. This process is known as destructuring, and allows users to "unpack" a collection's values into separate variables.
To make the section self-contained, we start by reviewing destructuring for regular tuples. This will provide a foundational understanding before exploring destructuring named tuples. Both approaches enable the creation of variables with specific values, although they cater to distinct situations.
Destructuring Collections Through Tuples
Given a collection list with two elements, destructuring allows the user to create variables x and y with the values of list. This requires running x,y = list, with the tuple on the left-hand side of =. The following examples illustrate this operation, according to several objects taken as list.
list = [3,4]
x,y = list
Output in REPL
julia>
x
3
julia>
y
4
list = 3:4
x,y = list
Output in REPL
julia>
x
3
julia>
y
4
list = (3,4)
x,y = list
Output in REPL
julia>
x
3
julia>
y
4
list = (a = 3, b = 4)
x,y = list
Output in REPL
julia>
x
3
julia>
y
4
In addition to destructuring all elements in list as above, we can also selectively destructure a subset of elements. When working with tuples, destructuring assigns values in a sequential order, without allowing any values to be skipped. As a result, if you're not interested in a particular element, you must explicitly indicate it. This requires assigning _ as a variable name, which signals that no assignment should be performed for that value.
list = [3,4,5]
(x,) = list
Output in REPL
julia>
x
3
list = [3,4,5]
x,y = list
Output in REPL
julia>
x
3
julia>
y
4
list = [3,4,5]
_,_,z = list # _ skips the assignment of that value
Output in REPL
julia>
z
5
list = [3,4,5]
x,_,z = list # _ skips the assignment of that value
Output in REPL
julia>
x
3
julia>
z
5
Named Tuples Destructuring
Consider now the case where list is a named tuple. Destructuring named tuples offers greater flexibility compared to regular tuples: you can extract key-value pairs without adhering to the order of elements and without explicitly identifying the values to be skipped.
To illustrate this, let's first consider destructuring a named tuple nt through a tuple on the left-hand side of the assignment. This will serve as a baseline for further discussions.
nt = (; name1 = 10, name2 = 20, name3 = 30)
x, y, z = nt
Output in REPL
julia>
x
10
julia>
y
20
julia>
z
30
nt = (; name1 = 10, name2 = 20, name3 = 30)
x, y = nt
Output in REPL
julia>
x
10
julia>
y
20
nt = (; name1 = 10, name2 = 20, name3 = 30)
_, _, z = nt # _ is notation to indicate we don't want the first two values of 'nt'
Output in REPL
julia>
z
30
Instead of using a tuple on the left-hand side, let's now utilize a named tuple. This makes it possible to specify the variables to be created by referencing their corresponding keys. The variable created will then be assigned the value associated with that key, regardless of the order in which you specify each variable.
nt = (; name1 = 10, name2 = 20, name3 = 30)
(; name2, name1) = nt # names in any order
Output in REPL
julia>
name1
10
julia>
name3
30
nt = (; name1 = 10, name2 = 20, name3 = 30)
(; name3) = nt
Output in REPL
julia>
name3
30
Remark
Be careful with mixing tuples and named tuples in destructuring. The following example shows the potential issue that could arise otherwise.
nt = (; name1 = 10, name2 = 20, name3 = 30)
name2, name1 = nt # variables defined according to POSITION
(name2, name1) = nt # alternative notation
Output in REPL
julia>
name2
10
julia>
name1
20
nt = (; name1 = 10, name2 = 20, name3 = 30)
(; name2, name1) = nt # variables defined according to NAME
; name2, name1 = nt # alternative notation
Output in REPL
julia>
name1
10
julia>
name2
20
The same caveat applies to a single-variable assignment.
nt = (; name1 = 10, name2 = 20)
(name2,) = nt # variable defined according to POSITION -> only first element
Output in REPL
julia>
name2
10
nt = (; name1 = 10, name2 = 20)
(; name2) = nt # variable defined according to NAME
Output in REPL
julia>
name2
20
An Application of Destructuring
Destructuring named tuples is particularly useful for passing multiple parameters to a function. If you need to repeatedly call a function like this, defining a named tuple to store all relevant parameters proves highly efficient. In this way, the named tuple serve as a single argument passed directly to the function. Then, inside the function, you can extract the specific parameters needed for each operation.
The approach eliminates the need for repetitively specifying a named tuple when a parameter is used, resulting in cleaner and more concise code.
parameters_list = (; β = 3, δ = 4, ϵ = 5)
# function 'foo' only uses 'β' and 'δ'
function foo(x, parameters_list)
x * parameters_list.δ + exp(parameters_list.β) / parameters_list.β
end
Output in REPL
julia>
foo(2, parameters_list)
14.695
parameters_list = (; β = 3, δ = 4, ϵ = 5)
# function 'foo' only uses 'β' and 'δ'
function foo(x, δ, β)
x * δ + exp(β) / β
end
Output in REPL
julia>
foo(2, parameters_list.β, parameters_list.δ)
14.695
parameters_list = (; β = 3, δ = 4, ϵ = 5)
# function 'foo' only uses 'β' and 'δ'
function foo(x, parameters_list)
(; β, δ) = parameters_list
x * δ + exp(β) / β
end