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
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
You can check that this function behaves as intended by
In [ ]:
sumSquares(-40)
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
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
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)
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
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).
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[:]
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')