3.3 Variables

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 of x above). In either situation you can check the type of a variable with typeof function, e.g. typeof(x) or typeof(zz).

3.3.1 Optional type declaration

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.).

3.3.2 Meaningful variable names

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.

3.3.3 Floats comparisons

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 float numbers cannot be represented exactly in binary (for technical details see this StackOverflow’s thread). 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:

We will see how to deal with the lack of precision in comparisons later (see Section 3.8.2).

3.3.4 Other types

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, e.g. 'a', 'b', 'c', …, 'z' (also uppercase) are individual characters. So 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 Chars. 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 and can take only two values: true or false (see the results of the comparison operations above in Section 3.3.3). Bools 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

3.3.5 Collections

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).

3.3.6 Vectors

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 the 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).

3.3.7 Arrays

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 means all indices in a row.

myGrades[1, :] # returns first row (and all columns)
[3.5, 3.0]

By analogy, the : symbol 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 Arrays (or Vectors 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) returns Tuple (see Tuple in the docs) and it is there to show both x and y in one line. You may think of Tuple as something similar to Vector but written with parenthesis () instead of square brackets []. Additionally, you cannot modify elements of a tuple after it was created (so, if you got z = (1, 2, 3), then z[2] will work fine (since it just returns an element), but z[2] = 8 will produce an error). Technically speaking, you could just type x, 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 on (reference to) the same box of drawers. 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])

3.3.8 Structs

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: Structs’ names are usually defined 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 Rational. For convenience the fraction is written as

1//2 # equivalent to: Rational(1, 2)

1//2

Notice the double slash character (//).

In general, structs 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.



CC BY-NC-SA 4.0 Bartlomiej Lukaszuk