Worksheet 3

Raising Errors

Long and complex programs are usually broken in several smaller scripts, functions, objects and types. It is important to think about possible complications when writing smaller blocks, and how to manage possible problems let's try for example our function sumSquares that computes \begin{equation} \sum_{i=0}^{n} i^2 \end{equation}


In [ ]:
function sumSquares(n)
    answer = 0.0;
    for i = 1:n
        answer += i^2
    end
    return answer
end
# MatLab version function answer = sumSquares(n) answer = 0; for i = 1:n answer = answer + i^2; end end

We can test this function using the following script


In [ ]:
n = 10
squares = [1:n].^2
result = sum(squares)

print("error  = ",result - sumSquares(n), "\n")

Explain why the function sumSquares shoud have a lower memory footprint than the script we just run?

Answer: In the script we need to allocate the whole vector $1:n$ which represents a memory footprint of $\mathcal{O}(n)$; whereas, the function sumSquares uses a for loop and only allocates a constant number of fields, then its memory footprint is $\mathcal{O}(1)$

For example, we can call the above function with a negative $n$. This operation is not well defined, then we should expect an abnormal behavior


In [ ]:
sumSquares(-1)

As you can see, the function is called, and nothing suspicious seems to be happening, but the answer is not even well defined.

Imagine that you have a big program that somewhere in its source code calls sumSquare, but with a negative number. The result of your program will be completely wrong, and it will be extremely hard to find the bug.

The best way to code functions is to restrict the usage of your code, and raise exceptions or errors. In that way, if your program does something that is not allowed, the program will stop and give you some kind of warning.

For example :


In [ ]:
function sumSquares(n)
    # to save space we will use an abreviation of an IF statement
    n < 0 && error("input for sumSquares needs to be a non negative integer \n")
    answer = 0.0;
    for i = 1:n
        answer += i^2
    end
    return answer
end
function answer = sumSquares(n) if n < 0 error('input for sumSquares needs to be a non negative integer') end answer = 0; for i = 1:n answer = answer + i^2; end end

You can check that this function behaves as intended by


In [ ]:
sumSquares(-40)

Optional parameters

Another useful techniques to debug your code and to add more functionality to your functions is to use optional parameters. These parameters are set to a default but they can be easily modified to add functionality to your functions.

This allows us to define a default behavior of the code, that can subsequently be modified by adding more parameters as needed.

In this case, we will modify the sumSquares, and add a "verbose" flag. If verbose is true, then the function will print all the intermediate steps of the computation.


In [ ]:
function sumSquares(n; verbose=false)
    n < 0 && error("input for sumSquares needs to be a non negative integer \n")
    verbose && print("Computing the sum of the ", n, " first squared integers \n")
    answer = 0.0;
    for i = 1:n
        answer += i^2
        verbose && print(answer, "\n") # abreviation of if (verbose) print(answer, "\n") end
    end
    return answer
end
% MatLab version function answer = sumSquares(n,verbose) % if the function only has one paramenter % we need to be sure that verbose is well defined if nargin < 2 % setting default value for verbose verbose = false; end if n < 0 error('input for sumSquares needs to be a non negative integer') end answer = 0; for i = 1:n answer = answer + i^2; if verbose fprintf('%d \n', answer) else end end

By default the flag is false, and the behavior of the function is the same as before


In [ ]:
sumSquares(10)

We can activate the new flag using the following notation


In [ ]:
sumSquares(10, verbose=true)

This can become handy when debugging a function. And allows to add more functionality to your functions.

Even though printinf in the console can help the debugging, you can also add a history parameter, such that the function outputs a vector with all the intermediate results.


In [ ]:
function sumSquares(n; verbose=false, history=false)
    n < 0 && error("input for sumSquares needs to be a non negative integer \n")
    verbose && print("Computing the sum of the ", n, " first squared integers \n")
    answer = 0.0;
    history && (histAnswer = zeros(n,1)) # if history is true then create a vector full of zeros
    for i = 1:n
        answer += i^2
        verbose && print(answer, "\n") # abreviation of if (verbose) print(answer, "\n") end
        history && (histAnswer[i] = answer) # if history is true save the current value of answer 
    end
    history? (return histAnswer) : (return answer)
end
function answer = sumSquares(n,verbose, history) if nargin < 3 history = false; end if nargin < 2 verbose = false; end if n < 0 error('input for sumSquares needs to be a non negative integer') end answer = 0 for i = 2:n if history answer(i) = answer(i-1) + i^2; else answer = answer + i^2; end end end

You can test your new function


In [ ]:
sumSquares(10,history=true)

Given that the information is in a vector you can manipulate it, and, in particular, plot it.


In [ ]:
n = 10
answerHist = sumSquares(n,history=true);
using Gadfly
plot(x = [1:n], y = answerHist)

Manipulation of vector and matrices

One of the other powerfull capabilities of Julia is its easiness to manipulate matrices.
You can declare and initialize a matrix of size $n\times n$ by tiping


In [ ]:
n = 4
M = zeros(n,n)

By default it builds a $n \times n$ matrix with each entry defined a Float64. You can extract the size of any matrix using the function size


In [ ]:
sizeM = size(M)

You can change the type of entries of the matrix using:


In [ ]:
M = zeros(Complex{Float64} , n, n)

As you can see, it defines an array of complex numbers.

To access any entry of the matrix you can use the index operation


In [ ]:
M[1,2]

We accessed the index (1,2) of the matrix. You can modify the matrix by using the index operation


In [ ]:
M[1,2] = 10
M

You can also modify a full column, by using the slicing ":" operator


In [ ]:
M[:,3] = 1:n
M

or a full row


In [ ]:
M[3,:] = (1:n)'
M

You need to ensure that the slice you are modifying has the same dimensions that the modification. In the example above, you build the vector $1:n$, which by default is a column vector. However, you want to modify a row of the matrix $M$, so you need to transpose $1:n$, using the transpose operator ' in order to have matching sizes.

You can modify portions of a matrix by using a range type (i.e. a $p:q$ type)


In [ ]:
# rand(n,m) will ouput a realisation of a random matrix n x m, with entries i.i.d. with uniform
# distribution between 0 and 1
M[2, 2:end] = rand(1,n-1) + im*rand(1,n-1)  # im is the Julia syntax for \sqrt{-1}
M

Or you can modify a block of the matrix


In [ ]:
M[1:3, 2:end] = rand(3,n-1) + im*rand(3,n-1)  # im is the Julia syntax for \sqrt{-1}
M

Creating Diagonal Matrices

Finally, you can create diagonal matrices by using


In [ ]:
v = rand(10,1); 
diagm(v[:], 0)

and to create a matrix M, with the vector v in its lower diagonal


In [ ]:
v = rand(9,1); 
diagm(v[:], -1)

Julia is a bit picky with respect to Matrices and vectors. Matrices are by default two dimensional array of type Array{Float64,2}, whereas vectors as one dimensional arrays Array{Float64,1}. You can convert a Matrix of size $n \times 1$ to a vector by using the [:] command (see the snippet above).

Matrices and vectors by direct input

Instead of creating a matrix full of zeros and then filling the entries,you can create your matrices by direct input. A space is used to separate elements within a row, and a semicolon is used to start a new row.


In [ ]:
A = [1 2 3; 4 5 6]

In an analogous way you can define a vector (in fact a $1 \times n$ matrix), by using direct input


In [ ]:
a = [ 1.0 2.0 3.0 4.0]

You can access, modify and permute the entries of a by using index operations


In [ ]:
print(a[1], "\n") # access first element of a
a[1] = 1000       # modify first element of a
print(a, "\n")
a = a[end:-1:1]   # flip the order of the elements of a
print(a, "\n")

Finally, you can transform this two dimensional array to a one dimensional array using the "[:]" command


In [ ]:
a[:]

Increasing the size of a vector

In Julia, contrary to MATLAB, the size of a vector are fixed, if you try, for example


In [ ]:
a[5] = 4

you will get an error.

If you need to increase the size of a vector you will use the push! function. However, push! is only defined for 1d vectors so we need to convert $\mathbf{a}$ from an Array{Float64,2} to a Array{Float64,1} using [:]


In [ ]:
a = a[:]
push!(a,5.0)

As you can see, we added a new element to the 1d array a. Push is mostly usefull when you have an array and you don't know its final length a priori. You can start with an empty array and add elements as needed.

To define an empty array you need to declare and specify the type of elements that your array will have, using the syntax Type[], so if you want to have an array of Float64 you need to write


In [ ]:
b = Float64[]

Then you can start pushing elements inside your array


In [ ]:
push!(b,3.0)
push!(b, pi)

We have seen that rather subtle difference between 1 and 2 d array. We encourage you to be extra careful when working with vectors (1d arrays) and Matrices (2d arrays). By default, vectors are colum vectors, so for example if you want to compute the dot and exterior product you can use the transpose operation coupled with the standard matrix multiplication (however, you need to note that using the transpose operation will convert your 1d array to a 2d array)


In [ ]:
a'

you can compute the dot and exterior product


In [ ]:
print("dot product = ",a'*a, "\n")
print("exterior product = \n",a*a')