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 violate the definition.
<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 |
Our previous discussions on collections have centered around vectors and tuples. The current section expands on the subject, offering a more comprehensive analysis of tuples and introducing two new types of collections: named tuples and dictionaries.
In particular, we'll cover how to characterize collections through keys and values, methods for the manipulation of collections, and approaches to transforming one collection into another.
Most collections in Julia are characterized by keys. They serve as unique identifiers of their elements, and have a corresponding value associated with each. [note] Not all collections map 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
as 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). Instead, indices exclusively employ integers as identifiers.
To identify the keys and values of a collection, Julia offers the functions keys
and values
. The following code snippets demonstrate their usage based on vectors and tuples, whose keys are represented by indices. Note that neither keys
nor values
return a vector, requiring the collect
function for this purpose.
x = [4, 5, 6]
collect(keys(x))
3-element Vector{Int64}:
1
2
3
collect(values(x))
3-element Vector{Int64}:
4
5
6
some_pair = Pair("a", 1) # equivalent
collect(keys(x))
"a" => 1
collect(values(x))
1
Pair
Collections of key-value pairs in Julia are represented by the type Pair{<key type>, <value type>}
. Although we won't directly work with objects of this type, they form the basis for constructing other collections such as 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>)
, making Pair("a",1)
equivalent to the previous example. Finally, given a pair x
, its key can be accessed by either x[1]
or x.first
, while its value can be retrieved using 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
some_pair
"a" => 1
some_pair[1]
"a"
some_pair.first
"a"
some_pair = ("a" => 1) # or simply 'some_pair = "a" => 1'
some_pair = Pair("a", 1) # equivalent
some_pair
"a" => 1
some_pair[2]
1
some_pair.second
1
Symbol
The type used to represent keys can vary depending on the collection. An important type used as a key is Symbol
, which provides an efficient way to represent string-based keys. A symbol labeled x
is denoted :x
, and can be created from strings using the function Symbol(<some string>)
. [note] Symbol
also enables the creation of variables programmatically. For example, it can be employed for defining new columns in the package DataFrames
, which provides a table representation of data.
x = (a=4, b=5, c=6)
some_symbol
vector_symbols
3-element Vector{Symbol}:
:a
:b
:c
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 violate the definition.
Named tuples share several similarities with regular tuples, including their immutability. However, they also exhibit some notable differences. One of them is that the keys of named tuples are objects of type Symbol
, in contrast to the numerical indices used for regular tuples.
Named tuples also differ syntactically, requiring being 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)
nt
nt.a
nt[:a]
#alternative way to access 'a'# 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
nt
nt.a
nt[:a]
#alternative way to access 'a'
keys
and values
.
nt = (a=10, b=20)
collect(keys(nt))
values(nt)
It's possible to create named tuples from 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)
nt
tup
x = 10
nt = (; x)
tup = (x, )
nt
tup
Dictionaries are collections of key-value pairs, exhibiting three distinctive features:
The keys of dictionaries can be any object: strings, numbers, and other objects are possible.
Dictionaries are mutable: you can modify, add, and remove elements.
Dictionaries are unordered: keys don't have any order attached.
Dictionaries are created using function Dict
, with each argument representing key-value pairs denoted by <key> => <value>
.
some_dict = Dict(3 => 10, 4 => 20)
dict
Dict{Int64, Int64} with 2 entries:
4 => 20
3 => 10
dict[1]
10
dict = Dict("a" => 10, "b" => 20)
dict
Dict{String, Int64} with 2 entries:
"b" => 20
"a" => 10
dict["a"]
10
some_dict = Dict(:a => 10, :b => 20)
dict
Dict{Symbol, Int64} with 2 entries:
:a => 10
:b => 20
dict[:a]
10
some_dict = Dict((1,1) => 10, (1,2) => 20)
dict
Dict{Tuple{Int64, Int64}, Int64} with 2 entries:
(1, 2) => 20
(1, 1) => 10
dict[(1,1)]
10
Note that regular dictionaries are inherently unordered, determining that access to 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 similarly to regular dictionaries, including their syntax, but endows the dictionary with an order.
some_dict = Dict(3 => 10, 4 => 20)
keys_from_dict = collect(keys(some_dict))
keys_from_dict
2-element Vector{Int64}:
4
3
some_dict = Dict("a" => 10, "b" => 20)
keys_from_dict = collect(keys(some_dict))
keys_from_dict
2-element Vector{String}:
"b"
"a"
some_dict = Dict(:a => 10, :b => 20)
keys_from_dict = collect(keys(some_dict))
keys_from_dict
2-element Vector{Symbol}:
:a
:b
some_dict = Dict((1,1) => 10, (1,2) => 20)
keys_from_dict = collect(keys(some_dict))
keys_from_dict
2-element Vector{Tuple{Int64, Int64}}:
(1, 2)
(1, 1)
Tuples, named tuples, and dictionaries can be constructed from other collections, provided that the source collection possesses a key-value structure. The following examples demonstrate how various collections can be used to create dictionaries in particular.
vector = [10, 20] # or tupl = (10,20)
dict = Dict(pairs(vector))
dict
Dict{Int64, Int64} with 2 entries:
2 => 20
1 => 10
keys_for_dict = [:a, :b]
values_for_dict = [10, 20]
dict = Dict(zip(keys_for_dict, values_for_dict))
dict
Dict{Symbol, Int64} with 2 entries:
:a => 10
:b => 20
keys_for_dict = (:a, :b)
values_for_dict = (10, 20)
dict = Dict(zip(keys_for_dict, values_for_dict))
dict
Dict{Symbol, Int64} with 2 entries:
:a => 10
:b => 20
nt_for_dict = (a = 10, b = 20)
dict = Dict(pairs(nt_for_dict))
dict
Dict{Symbol, Int64} with 2 entries:
:a => 10
:b => 20
keys_for_dict = (:a, :b)
values_for_dict = (10, 20)
vector_keys_values = [(keys_for_dict[i], values_for_dict[i]) for i in eachindex(keys_for_dict)]
dict = Dict(vector_keys_values)
dict
Dict{Symbol, Int64} with 2 entries:
:a => 10
:b => 20
Likewise, we can define a tuple from other collections, as shown below.
a = 10
b = 20
tup = (a, b)
tup
(10, 20)
values_for_tup = [10, 20]
tup = (values_for_tup... ,)
tup
(10, 20)
values_for_tup = [10, 20]
tup = Tuple(values_for_tup)
tup
(10, 20)
Finally, named tuples can be constructed from other collections.
a = 10
b = 20
nt = (; a, b)
nt
(a = 10, b = 20)
keys_for_nt = [:a, :b]
values_for_nt = [10, 20]
nt = (; zip(keys_for_nt, values_for_nt)...)
nt
(a = 10, b = 20)
keys_for_nt = [:a, :b]
values_for_nt = [10, 20]
nt = NamedTuple(zip(keys_for_nt, values_for_nt))
nt
(a = 10, b = 20)
keys_for_nt = (:a, :b)
values_for_nt = (10, 20)
nt = NamedTuple(zip(keys_for_nt, values_for_nt))
nt
(a = 10, b = 20)
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)
nt
(a = 10, b = 20)
dict = Dict(:a => 10, :b => 20)
nt = NamedTuple(vector_keys_values)
nt
(a = 10, b = 20)
Previously, we've demonstrated how to create a tuple and a named tuple from variables. Next, we show that the reverse operation is also possible, where values are extracted from a tuple or named tuple and assigned to individual variables. This process is known as destructuring, and allows users to "unpack" the values of a collection into separate variables.
Destructuring involves the assignment operator =
, with a tuple or named tuple on the left-hand side. The key difference between them lies in their compatibility with other collections: named tuples on the left-hand side require a matching named tuple, whereas tuples can be paired with a variety of collection types on the right-hand side. We illustrate each case below.
Given a collection list
with two elements, destructuring enables the user to create variables x
and y
with the values of list
. This is implemented by the syntax <tuple> = <collection>
, such as x,y = list
. The following examples illustrate this operation, according to different objects taken as list
.
list = [3,4]
x,y = list
x
y
list = 3:4
x,y = list
x
y
list = (3,4)
x,y = list
x
y
list = (a = 3, b = 4)
x,y = list
x
y
In addition to destructuring all elements in list
, you can choose to destructure only a subset of elements. The assignment is then performed in sequential order, following the collection's inherent order, without the possibility of skipping any specific value. To explicitly disregard a value, it's common to use the special variable name _
as a placeholder. Note that this is merely a convention, without any impact on execution.
For illustration purposes, we'll use a vector as an example of list
, but the same principle applies to any object.
list = [3,4,5]
(x,) = list
x
list = [3,4,5]
x,y = list
x
y
list = [3,4,5]
_,_,z = list # _ skips the assignment of that value
z
list = [3,4,5]
x,_,z = list # _ skips the assignment of that value
x
z
An alternative to standard tuples for destructuring is given by employing named tuples on the left-hand side. This approach lets you extract values by directly referencing field names, rather than relying on their positional order. Its key advantage is that variables can be assigned values in any order, provided their names correspond to the field names in the named tuple.
nt = (; key1 = 10, key2 = 20, key3 = 30)
(; key2, key1) = nt # keys in any order
key1
key3
nt = (; key1 = 10, key2 = 20, key3 = 30)
(; key3) = nt # only one key
key3
nt = (; key1 = 10, key2 = 20, key3 = 30)
key2, key1 = nt # variables defined according to POSITION
(key2, key1) = nt # alternative notation
key2
key1
nt = (; key1 = 10, key2 = 20, key3 = 30)
(; key2, key1) = nt # variables defined according to KEY
; key2, key1 = nt # alternative notation
key1
key2
The same caveat applies to single-variable assignments.
nt = (; key1 = 10, key2 = 20)
(key2,) = nt # variable defined according to POSITION
key2
nt = (; key1 = 10, key2 = 20)
(; key2) = nt # variable defined according to KEY
key2
Destructuring named tuples is especially useful in models that involve a repeated use of numerous parameters. By storing all these parameters in a named tuple, you can pass a single argument to functions. Then, by destructuring the named tuple, you can extract the needed parameters at the beginning of the function body.
β = 3
δ = 4
ϵ = 5
# function 'foo' only uses 'β' and 'δ'
function foo(x, δ, β)
x * δ + exp(β) / β
end
foo(2, δ, β)
parameters_list = (; β = 3, δ = 4, ϵ = 5)
# function 'foo' only uses 'β' and 'δ'
function foo(x, parameters_list)
x * parameters_list.δ + exp(parameters_list.β) / parameters_list.β
end
foo(2, parameters_list.β, parameters_list.δ)
parameters_list = (; β = 3, δ = 4, ϵ = 5)
# function 'foo' only uses 'β' and 'δ'
function foo(x, parameters_list)
(; β, δ) = parameters_list
x * δ + exp(β) / β
end
foo(2, parameters_list)
Another useful application of destructuring occurs when we need to retrieve multiple outputs of a function. This enables you to store each result in a separate variable. Below, we illustrate this application with a tuple and variables x
, y
, and z
.
function foo()
out1 = 2
out2 = 3
out3 = 4
out1, out2, out3
end
x, y, z = foo()
function foo()
out1 = 2
out2 = 3
out3 = 4
[out1, out2, out3]
end
x, y, z = foo()
Another typical application of destructuring is when we need only a subset of a function's outputs. While both tuples and named tuples can be applied for this purpose, the former offer more flexibility since they can be combined with various types of collections. Instead, named tuples are limited to another named tuple as the function's output, further requiring prior knowledge of the output's field names.
The following example demonstrates this functionality by extracting the first and third output of the foo
function.
function foo()
out1 = 2
out2 = 3
out3 = 4
out1, out2, out3
end
x, _, z = foo()
function foo()
out1 = 2
out2 = 3
out3 = 4
[out1, out2, out3]
end
x, _, z = foo()
function foo()
out1 = 2
out2 = 3
out3 = 4
(; out1, out2, out3)
end
(; out1, out3) = foo()