Macro 318: Lecture 1 - 3 / Tutorial 1


Introduction to Programming with Julia

Lecturer:
Dawie van Lill (dvanlill@sun.ac.za)

Introduction

Your interest in computational work will determine how much of this guide you read.

  • Very interested -- Read the entire full notebook (even if you don't understand)
  • Somewhat interested -- Read through the full notebook and scan through the optional sections
  • No interest -- Only read the short notes (compulsory) so that you can complete the tutorials

Try not to let the amount of information overwhelm you.

The notebook is meant to be used as a reference

The slides / short notes presented here are the compulsory part of the tutorial.

Lecture / Tutorial topics

Here are some of the focus areas

  1. Fundamentals of programming with Julia -- Lecture 1 - 3 / Tutorial 1
  2. Data, math and stats -- Lecture 4 - 6 / Tutorial 2
  3. Optimisation and the consumer problem -- Lecture 7 - 12 / Tutorial 3
  4. Solow model -- Lecture 13 - 16 / Tutorial 4

Running the notebooks

It is preferred that you install the programs on your computer.

This requires that you install Anaconda and Julia.

Here is a link that explains the installation process.

However, if you are finding it difficult to get things working you may try the other options.

I will make a brief video on how to install Anaconda and link it to Julia.

Lecture outline

  1. Variables
  2. Data structures
  3. Control flow
  4. Functions
  5. Visualisation
  6. Type system and generic programming (optional)

Your first code!

Before we start our discussion, let us try and run our first Julia program.

For those that have done programming before, this normally entails writing a piece of code that gives us the output Hello World!.

In Julia this is super easy to do.

In [1]:
println("Hello World!")
Hello World!

Introduction to packages

Julia has many useful packages. If we want to include a specific package then we can do the following,

import Pkg

Pkg.add("PackageName")

using PackageName

In [ ]:
import Pkg

Pkg.add("Plots") # Package for plotting

using Plots

Variables and types

After having successfully written your Hello World! code in Julia, a natural place to continue your journey is with variables.

A variable in a programming language is going to be some sort of symbol that we assign some value.

In [3]:
x = 2 # We assign the value of 2 to the variable x
Out[3]:
2
In [4]:
typeof(x) # Command to find the type of the x variable
Out[4]:
Int64

We see that the type of the variable is Int64.

What is an Int64?

Variables and types

We can now work with x as if it represents the value of 2.

Since an integer is a number, we can perform basic mathematical operations.

In [5]:
y = x + 2
Out[5]:
4
In [6]:
typeof(y)
Out[6]:
Int64

Variables and types

We can reassign the variable x to another value, even with another type.

In [7]:
x = 3.1345
Out[7]:
3.1345
In [8]:
typeof(x)
Out[8]:
Float64

Now x is a floating point number.

What is a floating point number?

This is an approximation to a decimal (or real) number.

Primitive data types

There are several important data types that are at the core of computing. Some of these include,

  • Booleans: true and false
  • Integers: -3, -2, -1, 0, 1, 2, 3, etc.
  • Floating point numbers: 3.14, 2.95, 1.0, etc.
  • Strings: "abc", "cat", "hello there"
  • Characters: 'f', 'c', 'u'

Arithmetic operators

We can perform basic arithmetic operations.

Operators perform operations.

These common operators are called the arithmetic operators.

Expression Name Description
x + y binary plus performs addition
x - y binary minus performs subtraction
x * y times performs multiplication
x / y divide performs division
x ÷ y integer divide x / y, truncated to an integer
x \ y inverse divide equivalent to y / x
x ^ y power raises x to the yth power
x % y remainder equivalent to rem(x,y)

Arithmetic operators

Here are some simple examples that utilise these arithmetic operators.

In [9]:
x = 2; y = 10;
In [10]:
x * y
Out[10]:
20
In [11]:
x ^ y
Out[11]:
1024
In [12]:
y / x # Note that division converts integers to floats
Out[12]:
5.0
In [13]:
2x - 3y
Out[13]:
-26
In [14]:
x // y
Out[14]:
1//5

Augmentation operators

Augmentation operators will be especially important in the section on control flow.

In [15]:
x += 1 # same as x = x + 1
Out[15]:
3
In [16]:
x *= 2 # same as x = x * 2
Out[16]:
6
In [17]:
x /= 2 # same as x = x / 2
Out[17]:
3.0

Comparison operators

These operators help to generate true and false values for our conditional statements

Operator Name
== equality
!=, inequality
< less than
<=, less than or equal to
> greater than
>=, greater than or equal to
In [18]:
x = 3; y = 2;
In [19]:
x < y # If x is less than y this will return true, otherwise false
Out[19]:
false
In [20]:
x != y # If x is not equal to y then this will return true, otherwise false
Out[20]:
true
In [21]:
x == y # If x is equal to y this will return true, otherwise false
Out[21]:
false

Data Structures

There are several types of containers, such as arrays and tuples, in Julia.

These containers contain data of different types.

We explore some of the most commonly used containers here.

Note that we have both mutable and immutable containers in Julia.

Tuples

Let us start with one of the basic types of containers, which are referred to as tuples.

These containers are immutable, ordered and of a fixed length.

In [22]:
x = (10, 20, 30)
Out[22]:
(10, 20, 30)
In [23]:
x[1] # First element of the tuple
Out[23]:
10
In [24]:
a, b, c = x # With this method of unpacking we have that a = 10, b = 20, c = 30
Out[24]:
(10, 20, 30)
In [25]:
a
Out[25]:
10

Arrays

One of the most important containers in Julia are arrays.

You will use tuples and arrays quite frequently in your code.

An array is a multi-dimensional grid of values.

Vectors and matrices, such as those from mathematics, are types of arrays in Julia.

One dimensional arrays (vectors)

A vector is a one-dimensional array.

A column (row) vector is similar to a column (row) of values that you would have in Excel.

Column vector is a list of values separated with commas.

Row vector is a list of values separated with spaces.

In [26]:
col_vector = [1, "abc"] # example of a column vector (one dimensional array)
Out[26]:
2-element Vector{Any}:
 1
  "abc"
In [27]:
not_col_vector = 1, "abc" # this creates a tuple! Remember the closing brackets for a vector.
Out[27]:
(1, "abc")
In [28]:
row_vector = [1 "abc"] # example of a row vector (one dimensional array)
Out[28]:
1×2 Matrix{Any}:
 1  "abc"

One dimensional arrays (vectors)

The big difference between a tuple and array is that we can change the values of the array.

Below is an example where we change the first component of the array.

This means that arrays are mutable.

In [29]:
col_vector[1] = "def"
Out[29]:
"def"
In [30]:
col_vector
Out[30]:
2-element Vector{Any}:
 "def"
 "abc"

You can now see that the first element of the vector has changed from 1 to def.

Mutating with push!()

We can use the push!() function to add values to this vector.

This grows the size of the vector.

You might notice the ! operator after push.

This exclamation mark doesn't do anything particularly special in Julia.

It is a coding convention to let the user know that the input is going to be altered / changed.

In our case it lets us know that the vector is going to be mutated.

Let us illustrate with an example.

Mutating with push!()

Let us illustrate how the push!() functions works with an example.

In [31]:
push!(col_vector, "hij") # col_vector is mutated here. It changes from 2 element vector to 3 element vector.
Out[31]:
3-element Vector{Any}:
 "def"
 "abc"
 "hij"
In [32]:
push!(col_vector, "klm") # We can repeat and it will keep adding to the vector.
Out[32]:
4-element Vector{Any}:
 "def"
 "abc"
 "hij"
 "klm"

Creating arrays

One easy way to generate an array is using a sequence.

We show multiple ways below to do this.

Note: If you want to store the values in an array, you need to use the collect() function.

In [33]:
seq_x = 1:10:21 # This is a sequence that starts at one and ends at 21 with an increment of 10.
Out[33]:
1:10:21
In [36]:
collect(seq_x) # Collects the values for the sequence into a vector
Out[36]:
3-element Vector{Int64}:
  1
 11
 21
In [37]:
seq_y = range(1, stop = 21, length = 3) # Another way to do the same as above
Out[37]:
1.0:10.0:21.0

Creating arrays

For creation of arrays we frequently use functions like zeros(), ones(), fill() and rand().

In [39]:
zeros(3) # Creates a column vector of zeros with length 3.
Out[39]:
3-element Vector{Float64}:
 0.0
 0.0
 0.0
In [40]:
zeros(Int, 3) # We can specify that the zeros need to be of type `Int64`.
Out[40]:
3-element Vector{Int64}:
 0
 0
 0
In [41]:
ones(3) # Same thing as `zeros()`, but fills with ones
Out[41]:
3-element Vector{Float64}:
 1.0
 1.0
 1.0

Creating arrays

For creation of arrays we frequently use functions like zeros(), ones(), fill() and rand().

In [42]:
fill(2, 3) # Fill a three element column with the value of `2`
Out[42]:
3-element Vector{Int64}:
 2
 2
 2
In [43]:
rand(3) # Values chosen will lie between zero and one. chosen with equal probability.
Out[43]:
3-element Vector{Float64}:
 0.6267475868435357
 0.44769771206451825
 0.08307481784542958
In [44]:
randn(3) # Values chosen randomly from Normal distribution
Out[44]:
3-element Vector{Float64}:
  0.8870407071510156
 -2.000543066533293
 -0.15256766820653117

Two dimensional arrays (matrices)

We can also create matrices (two dimensional arrays) in Julia.

A matrix has both rows and columns.

This would be like a table in Excel with rows and columns.

To create a matrix we separate rows by spaces and columns by semicolons.

In [45]:
matrix_x = [1 2 3; 4 5 6; 7 8 9] # Rows separated by spaces, columns separated by semicolons.
Out[45]:
3×3 Matrix{Int64}:
 1  2  3
 4  5  6
 7  8  9
In [46]:
matrix_y = [1 2 3;
            4 5 6;
            7 8 9] # Another way to write the matrix above
Out[46]:
3×3 Matrix{Int64}:
 1  2  3
 4  5  6
 7  8  9

Two dimensional arrays (matrices)

Those of you who have done statistics or mathematics know how important matrices are.

Matrices are a fundamental part of linear algebra.

Linear algebra is a super important area in mathematics (one of my favourites).

There is an optional section on linear algebra in the full tutorial notes.

If you want to do Honours in Economics, then I suggest getting comfortable with linear algebra!

Creating two dimensional arrays

We can also create two dimensional arrays with zeros(), ones(), fill() and rand().

In [47]:
zeros(3, 3)
Out[47]:
3×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0
In [48]:
ones(3, 3)
Out[48]:
3×3 Matrix{Float64}:
 1.0  1.0  1.0
 1.0  1.0  1.0
 1.0  1.0  1.0
In [49]:
randn(3, 3)
Out[49]:
3×3 Matrix{Float64}:
 -0.911564    1.75852   0.575633
 -0.671038   -0.956759  0.207368
  0.0351418  -1.21317   0.159683

Indexing

Remember from before that we can extract value from containers.

In [50]:
col_vector[1] # Extract the first value
Out[50]:
"def"
In [51]:
matrix_x[2, 2] # Retrieve the value in the second row and second column of the matrix
Out[51]:
5
In [52]:
col_vector[2:end] # Extracts all the values from the second to the end of the vector
Out[52]:
3-element Vector{Any}:
 "abc"
 "hij"
 "klm"
In [53]:
col_vector[:, 1] # Provides all the values of the first column
Out[53]:
4-element Vector{Any}:
 "def"
 "abc"
 "hij"
 "klm"

Broadcasting

One important topic that we need to think about is broadcasting.

Suppose you have a particular vector, such as [1, 2, 3].

You might want to apply a certain operation to each of the elements in that vector (elementwise).

Perhaps you want to find out what the sin of each of those values are independently?

Broadcasting

How would you do this? Well you could write you own loop that does this.

We will cover loops in a bit, so don't be concerned if you don't understand.

In [54]:
x_vec = [1.0, 2.0, 3.0];

# Loop for elementwise sin operation on `x` vector.
y_vec = similar(x_vec)
for (i, x) in enumerate(x_vec)
    y_vec[i] = sin(x)
end

y_vec # This now gives you sin of the `x` vector
Out[54]:
3-element Vector{Float64}:
 0.8414709848078965
 0.9092974268256817
 0.1411200080598672

Writing a loop like this for a simple operation seems wasteful.

Broadcasting

Instead of writing the loop, we can use the dot operator.

The dot operator broadcasts the function across the elements of the array.

Let us see some examples.

In [55]:
sin.(x_vec) # Notice the dot operator. What happens without the dot operator?
Out[55]:
3-element Vector{Float64}:
 0.8414709848078965
 0.9092974268256817
 0.1411200080598672
In [56]:
(x_vec).^2
Out[56]:
3-element Vector{Float64}:
 1.0
 4.0
 9.0
In [57]:
(x_vec) .* (x_vec)
Out[57]:
3-element Vector{Float64}:
 1.0
 4.0
 9.0

Control flow

In this section we will be looking at conditional statements and loops.

Conditional statements provide branches to a program depending on a certain condition.

The most recognisable conditional statement is the if-else statement.

In [58]:
x = 1

if x < 2
    print("first")
elseif x > 4
    print("second")
elseif x < 0
    print("third")
else
    print("fourth")
end
first

Loops

Avoid repeating code at ALL COSTS. Do not repeat yourself. This is the DRY principle in coding.

We illustrate this with an example.

In [59]:
x = [0,1,2,3,4]
y_1 = [] # empty array (list in python)

append!(y_1, x[1] ^ 2)
append!(y_1, x[2] ^ 2)
append!(y_1, x[3] ^ 2)
append!(y_1, x[4] ^ 2)
append!(y_1, x[5] ^ 2)
Out[59]:
5-element Vector{Any}:
  0
  1
  4
  9
 16

In order to fill out the y_1 array we had to write the same command five times.

Notice that the only thing that changes is the value of the index from the x array

Loops

There must be easier way to fill this y_1 array.

We will try to use a for loop to fill this array.

In [60]:
x = [0,1,2,3,4]
y_1 = [] # Let's empty this array again and try another method

for i in x
    append!(y_1, i ^ 2)
    println(y_1)
end
Any[0]
Any[0, 1]
Any[0, 1, 4]
Any[0, 1, 4, 9]
Any[0, 1, 4, 9, 16]

Comprehensions

There is a more compact way to fill our array using the for loop from above.

This is known as an array comprehension.

The code for the comprehension is as follows.

In [61]:
y_1 = [i^2 for i in x]
Out[61]:
5-element Vector{Int64}:
  0
  1
  4
  9
 16

Functions and methods

Functions are important to writing complex code.

You should always write in terms of functions if possible.

From mathematics we know that a function is an abstraction that takes in some input and returns an output.

In the world of computing the idea is similar.

Let us take a look at some functions below.

Functions and methods

The most common to way to write a function is as follows,

In [62]:
function f(x) # function header
    return x ^ 2 # body of the function
end
Out[62]:
f (generic function with 1 method)

This function accepts one input and returns one output.

Namely, it takes the value of x and then squares it.

In [63]:
f(2)
Out[63]:
4

Functions and methods

With the following function we have two inputs, x and y, that return one output.

In [64]:
function k(x, y)
    return x ^ 2 + y ^ 2
end
Out[64]:
k (generic function with 1 method)
In [65]:
k(2, 2)
Out[65]:
8

What is return?

return basically instructs the program to exit the function and return the specified value.

In [66]:
function return_example_1(x)

    if x > 5
        return x + 5 # this will exit the function immediately

        println(x - 9) # this will never execute, it is after the return
    end

    return x / 5 # if x <= then this will be returned
end
Out[66]:
return_example_1 (generic function with 1 method)
In [67]:
return_example_1(1)
Out[67]:
0.2
In [68]:
return_example_1(10) # notice that the `println(x - 9)` doesn't execute
Out[68]:
15

Scope

Global variables are generally considered to be a bad idea.

If you want to be a good programmer, you need to understand the basics of local scoping.

Most of the code in the notebook has been contained in the global scope.

If you are writing scripts and source code you must wrap code in functions to avoid variables entering the global scope.

Scope

When you copy variables inside functions, they become local and the function is said to become a closure.

We will show what we mean through some examples.

In [69]:
a = 2 # global variable

function f(x)
    return x ^ a
end;

function g(x; a = 2)
    return x ^ a
end;

function h(x)
    a = 2 # a is local 
    return x ^ a
end;
In [70]:
f(2), g(2), h(2)
Out[70]:
(4, 4, 4)

Scope

What happens in our previous example if we change the value for a?

Which of the outputs for the functions will be changed?

Can you see why this might be problematic?

In [71]:
a = 3
Out[71]:
3
In [72]:
f(2), g(2), h(2)
Out[72]:
(8, 4, 4)

Visualisation

Let us show the basic principles of plotting in Julia.

In [73]:
xs = 0:0.1:20;
ys = sin.(xs); # we are using dot syntax - indicates broadcasting over the entire sequence

plot(xs, ys)
Out[73]:
┌ Info: Precompiling GR_jll [d2c73de3-f751-5644-a686-071e5b155ba9]
└ @ Base loading.jl:1423

Visualisation

We can add titles, legends, colors and other components with keywords.

You can check the Plots.jl package documentation for the different keywords for plots

In [74]:
plot(xs, ys, color = :steelblue, title = "Our first plot", legend = false, lw = 1.5) # lw stands for line width
Out[74]:

Visualisation

Another nice feature of plotting is the ability to overlay plots on top of each other.

In [75]:
ys_1 = sin.(xs);
ys_2 = cos.(xs);

plot(xs, ys_1, lw = 1.5, ls = :solid)
plot!(xs, ys_2, lw = 1.5, ls = :dash)
Out[75]: