The way I see it a variable is a box to store some value.
Type
x = 1
mark it (highlight it with a mouse) and run by pressing Ctrl+Enter
.
This creates a variable (an imaginary box) named x
(x
is a label on the box) that contains the value 1
. The =
operator assigns 1
(right side) to x
(left side) [puts 1
into the box].
Note: Spaces around mathematical operators like
=
are usually not necessary. Still, they improve legibility of your code.
Now, somewhat below type and execute
x = 2
Congratulations, now the value stored in the box (I mean variable x
) is 2
(the previous value is gone).
Sometimes (usually I do this inside of functions, see Section 3.4) you may see variables written like that
z::Int = 4
or
zz::Float64 = 4.4
The ::
is a type declaration. Here by using ::Int
you promise Julia that you will store only integers (like: …, -1, 0, 1, …) in this box. Whereas by typing ::Float64
you declare to place only floats (like: …, 1.1, 1.0, 0.0, 2.2, 3.14, …) in that box.
Note: You can either explicitly declare a type (with
::
) or let Julia guess it (when it’s not declared, like in the case ofx
above). In either situation you can check the type of a variable withtypeof
function, e.g.typeof(x)
ortypeof(zz)
.
In Julia type declaration is optional. You don’t have to do this, Julia will figure out the types anyway. Still, sometimes it is worth to declare them (explanation in a moment). If you decide to do so, you should declare a variable’s type only once (the time it is first created and initialized with a value).
If you use a variable without a type declaration then you can freely reassign to it values of different types.
Note: In the code snippet below
#
and all the text to the right of it is a comment, the part that is ignored by a computer but read by a human.
a = 1 # type is not declared
a = 2.2 # can assign a value of any other type
# the "Hello" below is a string (a text in a form readable by Julia)
a = "Hello"
But you cannot assign (to a variable) a value of a different type than the one you declared (you must keep your promises). Look at the code below.
This is OK
b::Int = 1 # type integer declared
b = 2 # value of type integer delivered
But this is not OK (it’s wrong! it’s wroooong!)
c::Int = 1 # type integer declared
c = 3.3 # broke the promise, float delivered, it will produce an error
c = 3.1 # again, broke the promise, float delivered, expect error
Now a question arises. Why would you want to use a type declaration (like ::Int
or ::Float64
) at all?
In general you put values into variables to use them later. Sometimes, you forget what you placed there and may get an unexpected result (it may even go unnoticed for some time). For instance it makes more sense to use integer instead of string for some operations (e.g. I may wish to multiply 3
by 3
not "three"
by "three"
).
x = 3
x * x # works as you intended
9
x = "three"
x * x # the result may be surprising
threethree
Note: Julia gives you a standard set of mathematical operators, like addition (
+
), subtraction (-
), multiplication (*
), division (/
) and more (see the docs).
The latter is an example of a so called string concatenation, it may be useful (as we will see later in this book), but probably it is not what you wanted.
To avoid such unexpected events (especially if instead of *
you use your own function, see Section 3.4) you would like a guarding angel that watches over you. This is what Julia does when you require it by using type declarations (for now you need to take my word for it).
Moreover, declaring types sometimes may make your code run faster.
Additionally, some IDEs work better (improved code completions, and hints) when you place type declarations in your code.
Personally, I like to use type declarations in my own functions (see the upcoming Section 3.4) to help me reason what they do. At first I write functions without types at all (it’s easier that way). Once I got them running I add the types to them (it us useful for future reference, code maintenance, etc.).
Name your variables well. The variable names I used before are horrible (mea culpa, mea culpa, mea maxima culpa). We use named variables (like x = 1
) instead of ‘loose’ variables (you can type 1
alone in a script file and execute that line) to use them later.
You can use them later in time (reading and editing your code tomorrow or next month/year) or in space (using it 30 or 300 lines below). If so, the names need to be memorable (actually just meaningful will do :D). So whenever possible use: studentAge = 19
, bookTitle = "Dune"
(grammatical correctness is not that important) instead of x = 19
, y = "Dune"
.
You may want to check Julia’s Docs for the allowed variable names and their recommended stylistic conventions (for now, always start with a small letter, and use alphanumeric characters from the Latin alphabet). Personally, I prefer to use camelCaseStyle so this is what you’re gonna see here.
Be careful with =
sign. In mathematics =
means equal to
and ≠
means not equal to
. In programming =
is usually an assignment operator (see Section 3.3 before). If you want to compare for equality you should use ==
(for equal to
) and (!=
for not equal to
), examples:
1 == 1
true
2 == 1
false
2.0 != 1.0
true
# comparing float (1.0) with integer (1)
1.0 != 1
false
# comparing integer (2) with float (2.0)
2 == 2.0
true
Be careful though because the comparisons of two floats are sometimes tricky, e.g.
(0.1 * 3) == 0.3
false
The problem here is not Julia (go ahead, try (0.1 * 3) == 0.3
in another programming language), but computers in general. The result is false
since some floats cannot be represented exactly as binary numbers (used internally by a computer), just like the fraction \(\frac{1}{3}\) cannot be exactly represented in decimal numeral system (\(\frac{1}{3}\) = 0.333…). If you are interested in more technical details see this StackOverflow’s thread. Anyway, this is how my computer sees 0.1 * 3
:
0.1 * 3
0.30000000000000004
and 0.3
0.3
0.3
The same caution applies to other comparison operators, like:
x > y
(x
is greater than y
),x >= y
(x
is greater than or equal to y
),x < y
(x
is less than y
),x <= y
(x
is less than or equal to y
).We will see how to deal with the lack of precision in comparisons later (see Section 3.8.2).
There are also other types (see Julia’s Docs), but we will use mostly those mentioned in this chapter, i.e.:
The briefly aforementioned strings contain text of any kind. They are denoted by (optional type declaration) ::String
and you type them within double quotation marks ("any text"
). If you ever want to place "
in a string you need to use \
(backslash) before it [otherwise Julia will terminate the string on the second "
it encounters and throw an error (because it will be confused by the remaining, stray, characters)]. Moreover, if you wish the text to be displayed in the next line (e.g. in a figure’s title like the one in Section 4.7.3) you should place \n
in it. For instance:
title = "I enjoy reading\n\"Title of my favorite book\"."
println(title)
Displays:
I enjoy reading
"Title of my favorite book".
on the screen.
A string is composed of individual characters (d’ooh!). An individual character (type ::Char
) is enclosed between single quotation marks. For instance, 'a'
, 'b'
, 'c'
, …, 'z'
(also uppercase) are all individual characters. Whenever you want to type a single character you got a choice, either use 'a'
(single Char
) or "a"
(String
composed of one Char
). But when typing two or more characters that are ‘glued’ together you must use double quotations ("ab"
). In the rest of the book we will focus mostly on strings, still, a bit more knowledge never hurt anyone (or did it?). In Solution to exercise 5 from Section 5.8.5, we will see how to easily generate a complete alphabet (or a part of it, if you ever need one) with Char
s. If you want to know more about the Strings and Chars just click the links to the docs that are to be found in this sentence.
The last of the earlier referenced types (boolean) is denoted as ::Bool
(note that in Julia types’ names by convention start with a capital letter) and can take only two values: true
or false
(see the results of the comparison operations above in Section 3.3.3). Bool
s are often used in decision making in our programs (see the upcoming Section 3.5) and can be used with a small set of logical operators like AND (&&
)
# && returns true only if both values are true
# those return false:
# true && false
# false && true
# false && false
# this returns true:
true && true
true
OR (||
)
# || returns true if any value is true
# those return true:
# true || false
# false || true
# true || true
# this returns false:
false || false
false
and NOT (!
)
# ! flips the value to the opposite
# returns false: !true
# returns true
!false
true
Not only do variables may store a single value but they can also store their collections. The collection types that we will discuss here are Vector
(technically Vector
is a one dimensional Array
but don’t worry about that now), Array
and struct
(it is more like a composite type, but again at that moment we will not be bothered by that fact).
myMathGrades = [3.5, 3.0, 3.5, 2.0, 4.0, 5.0, 3.0]
[3.5, 3.0, 3.5, 2.0, 4.0, 5.0, 3.0]
Here I declared a variable that stores my mock grades.
The variable type is Vector
of numbers (each of type Float64
, run typeof(myMathGrades)
to check it). I could have declared its type explicitly as ::Vector{Float64}
. Instead I decided to let Julia figure it out.
You can think of a vector as a rectangular cuboid box with drawers (smaller cube shaped boxes). The drawers are labeled with consecutive numbers (indices) starting at 1 (we will get to that in a moment). The variable contains 7 grades in it, which you can check by typing and executing length(myMathGrades)
.
You can retrieve a single element of the vector by typing myMathGrades[i]
where i
is some integer (the aforementioned index). For instance:
myMathGrades[3] # returns 3rd element
3.5
or
myMathGrades[end] # returns last grade
# equivalent to: myMathGrades[7], but here I don't have to count elements
3.0
Be careful though, if You type a non-existing index like myMathGrades[-1]
, myMathGrades[0]
or myMathGrades[10]
you will get an error (e.g. BoundsError: attempt to access 7-element Vector{Float64} at index [0]
).
You can get a slice (a part) of the vector by typing
myMathGrades[[2, 5]] # returns Vector with 2nd, and 5th element
[3.0, 4.0]
or
myMathGrades[[2, 3, 4]] # returns Vector with 2nd, 3rd, and 4th element
[3.0, 3.5, 2.0]
or simply
myMathGrades[2:4] # returns Vector with three grades (2nd, 3rd, and 4th)
# the slicing is [inclusive:inclusive]
[3.0, 3.5, 2.0]
The 2:4
is Julia’s range generator, with default syntax start:stop
(both of which are inclusive). Assume that under the hood it generates a vector (check it by using collect function, e.g, just run collect(2:4)
). So, it gives us the same result as writing myMathGrades[[2, 3, 4]]
by hand. However, the range syntax is more convenient (less typing especially for broad ranges). Now, let’s say I want to print every other grade out of 100 grades, then I can go with oneHunderedGrades[1:2:end]
and voila, a magic happened thanks to the start:step:stop
syntax (collect(1:2:end)
returns a vector of indices like [1, 3, 5, 7, ..., 97, 99]
).
One last remark, You can change the elements that are in a vector like this.
myMathGrades[1] = 2.0
myMathGrades
[2.0, 3.0, 3.5, 2.0, 4.0, 5.0, 3.0]
or like that
myMathGrades[2:3] = [5.0, 5.0]
myMathGrades
[2.0, 5.0, 5.0, 2.0, 4.0, 5.0, 3.0]
Again, remember about proper indexing. What you put inside (right side) should be compatible with indexing (left side), e.g myMathGrades[2:3] = [2.0, 2.0, 2.0]
will produce an error (placing 3 numbers to 2 slots).
A Vector
is actually a special case of an Array
, a multidimensional structure that holds data. The most familiar (and useful) form of it is a two-dimensional Array
(also called Matrix
). It has rows and columns. Previously I stored my math grades in a Vector
, but most likely I would like a place to keep my other grades. Here, I create an array that stores my grades from math (column1) and chemistry (column2).
myGrades = [3.5 3.0; 4.0 3.0; 5.0 2.0]
myGrades
3×2 Matrix{Float64}:
3.5 3.0
4.0 3.0
5.0 2.0
I separated the values between columns with a space character and indicated a new row with a semicolon. Typing it by hand is not very interesting, but they come in handy as we will see later in the book.
As with vectors I can use indexing to get specific element(s) from a matrix, e.g.
myGrades[[1, 3], 2] # returns second column (rows 1 and 3) as Vector
[3.0, 2.0]
or
myGrades[:, 2] # returns second column (and all rows)
[3.0, 3.0, 2.0]
Above, the :
symbol (when placed alone) means all indices in a row.
myGrades[1, :] # returns first row (and all columns)
[3.5, 3.0]
By analogy, here the :
symbol (when placed alone) means all indices in a column.
myGrades[3, 2] # returns a value from third row and second column
2.0
I can also use the indexing to replace a particular element in a Matrix
. For instance.
myGrades[3, 2] = 5
myGrades
3×2 Matrix{Float64}:
3.5 3.0
4.0 3.0
5.0 5.0
or
myGrades[1:2, 1] = [5, 5]
myGrades
3×2 Matrix{Float64}:
5.0 3.0
5.0 3.0
5.0 5.0
As with a Vector
also here you must pay attention to proper indexing.
When dealing with Array
s (or Vector
s which are one dimensional arrays) one needs to be cautious not to change their contents accidentally.
In case of atomic variables the values are assigned/passed as copies (i.e. a new number 3
is put to the box, the old number in the variable x
is unaffected). Observe.
x = 2
y = x # y contains the same value as x
y = 3 # y is assigned a new value, x is unaffected
(x, y)
(2, 3)
Note: The
(x, y)
returnsTuple
(see Tuple in the docs) and it is there to show bothx
andy
in one line. You may think ofTuple
as something similar toVector
but written with parenthesis()
instead of square brackets[]
. Additionally, you cannot modify elements of a tuple after it was created (so, if you gotz = (1, 2, 3)
, thenz[2]
will work fine (since it just returns an element), butz[2] = 8
will produce an error). Technically speaking, you could just typex, y
and run the line to get a tuple (test it out), but I prefer to use parenthesis to be explicit.
However, the arrays are assigned/passed as references.
xx = [2, 2]
yy = xx # yy refers to the same box of drawers as xx
yy[1] = 3 # new value 3 is put to the first drawer of the box pointed by yy
# both xx, and yy are changed, cause both point at the same box of drawers
(xx, yy)
([3, 2], [3, 2])
As stated in the comments to the code snippet above, here both xx
and yy
variables point at (reference to) the same box of drawers (imagine the same box of drawers got two labels xx
and yy
stuck to it next to each other). So, when we change a value in one drawer, then both variables reflect the change. If we want to avoid that we can, e.g. make a copy of the Vector
/Array
like so:
xx = [2, 2]
# yy refers to a different box of drawers
# with the same (copied) numbers inside
yy = copy(xx)
yy[1] = 3 # this does not affect xx
(xx, yy)
([2, 2], [3, 2])
Another Julia’s type worth mentioning is struct. It is a composite type (so it contains other type(s) inside).
Let’s say I want to have a thing that resembles fractions that we know from mathematics. It should allow to store the data for numerator and denominator (\(\frac{numerator}{denominator}\)). Let’s use struct
for that
struct Fraction
numerator::Int
denominator::Int
end
fr1 = Fraction(1, 2)
fr1
Fraction(1, 2)
Note: By convention
Struct
s’ names start with a capital letter.
If I ever wanted to get a component of the struct
I can use the dot syntax, like so
fr1.numerator
1
Note: If you type
fr1.
and press TAB key then you should see a hint with the available field names. You may choose one with arrow keys and confirm it with Enter key.
or
fr1.denominator
2
Of course, as you probably have guessed, there is no need to define your own type for fraction since Julia is already equipped with one. It is called Rational. For convenience the fraction is written as
1//2 # equivalent to: Rational(1, 2)
1//2
Notice the double slash character (//
).
In general, struct
s are worth knowing. A lot of libraries (see Section 3.7) define their own struct
objects and we may want to extract their content using the dot syntax (as we probably sometimes will in the upcoming sections).
OK, enough about the variables, time to meet functions.