Contents¶
In this notebook, we will cover the basic parts of Julia needed later to learn parallel computing. In particular, we will learn about:
- Variables
- Functions
- Arrays
For a more general introduction to Julia see the nice tutorials made available by JuliaAcademy here or the official Julia educational resources here.
Using Jupyter notebooks in Julia¶
We are going to use Jupyter notebooks in this and other lectures. You probably have worked with notebooks (in Python). If not, here are the basic concepts you need to know to follow the lessons.
How to start a Jupyter notebook in Julia¶
To run a Julia Jupyter notebook, open a Julia REPL and type
julia> ]
pkg> add IJulia
julia> using IJulia
julia> notebook()
A new browser window will open. Navigate to the corresponding notebook and open it.
Running a cell¶
To run a cell, click on a cell and press Shift
+ Enter
. You can also use the "Run" button in the toolbar above.
1+3
4*5
As you can see from the output of previous cell, the value of the last line is displayed. We can suppress the output with a semicolon. Try it. Execute next cell.
1+3
4*5;
Cell order is important¶
Running the two cells below in reverse order won't work (try it).
foo() = "Well done!"
foo()
A very easy first exercise¶
Run the following cell. It contains definitions used later in the notebook.
function why_q1()
msg = """
In the first line, we assign a variable to a value. In the second line, we assign another variable to the same value. Thus, we have 2 variables associated with the same value. In line 3, we associate y to a new value (re-assignment). Thus, we have 2 variables associated with 2 different values. Variable x is still associated with its original value. Thus, the value at the final line is x=1.
"""
println(msg)
end
function why_q2()
msg = """
It will be 1 for very similar reasons as in the previous questions: we are reassigning a local variable, not the global variable defined outside the function.
"""
println(msg)
end
function why_q3()
msg = """
It will be 6. In the returned function f2, x is equal to 2. Thus, when calling f2(3) we compute 2*3.
"""
println(msg)
end
println("🥳 Well done! ")
REPL modes¶
This is particular to Julia notebooks. You can use package, help, and shell mode just like in the Julia REPL.
] add MPI
? print
; ls
a = 1
When assigning a variable, the value on the right hand side is not copied into the variable. It is just an association of a name with a value (much like in Python).
Re-assign a variable¶
We can re-assign a variable, even with a value of another type. However, avoid changing the variable type for performance reasons.
a = 2
a = 1.0
a = "Hi!"
Unreachable objects¶
When an object is not associated with a variable any more, it cannot be reached by the user. This can happen, e.g., when we re-assign a variable. Another case is when local variables, e.g in a function, go out of scope. The following line allocates a large array and assigns it to variable a
a = zeros(300000000);
If we re-assign the variable to another value, the large array will be inaccessible.
a = nothing
Garbage collector¶
Luckily, Julia has a garbage collector that deallocates unreachable objects. You don't need to bother about manual deallocation! Julia is not constantly looking for unreachable objects. Thus, garbage collection does not happen instantaneously, but it will happen at some point. You can also explicitly call the garbage collector, but it is almost never done in practice.
GC.gc()
Type declarations are optional¶
Julia knows the type of the object associated with a variable.
a = 1
typeof(a)
We can annotate types if we want, but this will not improve performance (except in very special situations). Thus, annotating types is not done in practice.
c::Int = 1
typeof(c)
If you annotate a variable with a type, then it cannot refer to objects of other types.
c = "I am a string"
We can use Unicode (UTF-8 encoding) characters in variables and function names.
🐱 = "I am a cat"
🐶 = "I am a dog"
🐱 == 🐶
We can also use Greek letters and other mathematical symbols. Just write the corresponding LaTeX command and press Tab
. For example: ω
is written \omega
+ Tab
ω = 1.234
sin(ω)
In fact, some useful mathematical constants are predefined in Julia with math Greek letters.
sin(π/2)
x = 1
y = x
y = 2
x
Run next cell to get an explanation of this question.
why_q1()
Functions¶
Julia is very much a functional programming language. In consequence, Julia is more centered on functions than on types. This is in contrast to object-oriented languages, which are more centered on types (classes). For instance, you don't need to know the details of the Julia type system to learn parallel programming in Julia, but you need to have a quite advanced knowledge of how Julia functions work.
Defining functions¶
Functions are defined as shown in next cell. The closing end
is necessary. Do not forget it! However, the return
is optional. The value of last line is returned by default. Indentation is recommended, but it is also optional. That's why the closing end
is needed.
function add(a,b)
return a + b
end
Once defined, a function can be called using bracket notation as you would expect.
add(1,3)
Broadcast syntax¶
We can apply functions to arrays element by element using broadcast (dot) syntax.
a = [1,2,3]
b = [4,5,6]
add.(a,b)
Mathematical operators can also be broadcasted (like in Matlab). Multiplying the vectors a * b
directly won't work. If we want to multiply element by element, we can use the broadcasted version below.
a .* b
function q(x)
x = 2
x
end
x = 1
y = q(x)
x
Run next cell to get an explanation of this question.
why_q2()
References¶
As you can see variables are passed "by value". Passing variables "by reference" is done using a reference.
function q!(x)
x[] = 2
x
end
x = Ref(1)
q!(x)
x[]
Defining functions (shorter way)¶
For short functions, we can skip the function
and end
keywords as follows.
add_short(a,b) = a+b
add_short(1,3)
Anonymous (lambda) functions¶
Since we can assign function to variables, it is not needed for a function to have a function name in many cases. We can simply create an anonymous function (i.e., a function without name) and assign it to a variable.
add_anonymous = (a,b) -> a+b
We can call the function by using the variable name.
add_anonymous(2.0,3.5)
Note that add_anonymous
is not a function name. It is just a variable associated with a function with no function name (well, it has a name technically, but with an arbitrary value).
nameof(add_anonymous)
nameof(add)
Functions are first-class objects¶
We can work with Julia functions like with any other type of object. For instance, we can assign functions to variables.
a = add
Now, we can call the function using the variable name.
a(4,5)
We can also create an array of functions (this will not work in Python).
funs = [+,-,*]
To call a specific function in the array, we index the array and then call the returned function
funs[2](2,3)
Higher-order functions¶
Higher order functions are functions that take and/or return other functions. And example is the count
function in Julia.
For instance, we can pass a user-defined function to count the number of even elements in an array.
func = i->i%2==0
a = [1,2,3,5,32,2,4]
count(func,a)
Do-blocks¶
There is yet another way to define anonymous functions. If a function takes a function in its first argument (like count
) we can skip the first argument, when calling the function, and define the function we want to pass in a do-block. This is useful, e.g., if we want to define a multi-line anonymous function. The two next cells are equivalent.
function f(i)
m = i%2
m != 0
end
count(f,[1,2,3,5,32,2,4])
count([1,2,3,5,32,2,4]) do i
m = i%2
m != 0
end
Returning multiple values¶
Julia functions always return a single variable. To return multiple values, we can wrap them in a tuple.
function divrem(a,b)
α = div(a,b)
β = rem(a,b)
(α,β)
end
The output is a tuple as expected, but we can recover the individual values by unpacking the tuple.
d,r = divrem(10,3)
d
r
Variable number of input arguments¶
Functions with multiple arguments are also supported. The following example iterates over the given arguments and prints them. args
is just a tuple with all arguments.
function showargs(args...)
for (i,arg) in enumerate(args)
println("args[$i] = $arg")
end
end
showargs(1,"Hi!",π)
showargs(6)
Positional and keyword arguments¶
Functions can combine positional and keyword arguments much like in Python, but keyword arguments start with semicolon ;
in Julia.
function foo(a,b;c,d)
println("Positional: a=$a, b=$b. Keyword: c=$c, d=$d")
end
foo(3,4,d=2,c=1)
Optional arguments¶
We can provide default values to arguments to make them optional.
function bar(a,b=0;c,d=1)
println("Positional: a=$a, b=$b. Keyword: c=$c, d=$d")
end
bar(1,c=2)
function hofun(x)
y -> x*y
end
f2 = hofun(2)
x = f2(3)
x
Run next cell to get an explanation of this question.
why_q3()
Arrays¶
Julia supports multi-dimensional arrays. They are very similar to Numpy arrays in Python. Let's learn the basics of Julia arrays.
Array literals¶
We can create (small) arrays from the given values using array literals.
Next cell creates a vector with 3 integers. Note for Python users: there is no difference between vectors and lists in Julia.
vec = [1,2,3]
We can create a matrix as follows.
mat = [1 2 3 4
5 6 7 8
9 10 11 12]
Array initialization¶
We can create arrays with all the entries equal to zero, to one, or to a specific given value. The value can be any Julia object, even a function!
zeros(4)
zeros(Int,4)
ones(2,3)
fill(5.0,3,4)
fill(add,3,4)
Array comprehensions¶
We can also create the items in the array using a loop within an array comprehension.
squares = [ i^2 for i in 1:8 ]
Indexing¶
We can get and set the items of an array by indexing the array.
squares[3]
squares[end]
squares[2:4]
squares[4] = 16
squares[2:3] = [4,9]
Immutable element type¶
Note that once set, the type of the elements in the array cannot be changed. If we try to set an item with an object of a different type, Julia will try to do a conversion, which can fail depending on the passed value.
a = [10,11,12,13]
a[2] = "Hi!"
Arrays of any element type¶
Arrays of fixed element type seem to be very rigid, right? Python list do not this limitation. However, we can use arrays of the Any
type, which are as flexible as Python lists, or even more since they can also contain functions.
a = Any[10,11,12,13]
a[3] = "HI!"
a
Loops¶
The loop in next cell visits the elements in a
one after the other.
a = [10,20,30,40]
for ai in a
@show ai
end
This loop visits the integers from 1 to the length of the array and indexes the array at each of these integers.
for i in 1:length(a)
ai = a[i]
@show (i,ai)
end
This loop "enumerates" the items in the array.
for (i,ai) in enumerate(a)
@show (i,ai)
end
Arrays indices are 1-based by default¶
Be aware of this if you are a C or Python user.
a = [10,20,30,40]
a[0]
Slicing allocates a new array¶
This is also different from Numpy in Python.
a = [1 2 3
4 5 6]
s = a[:,2]
s[2] = 0
a
Array views¶
If you want to modify the original array, use view
instead.
v = view(a,:,2)
v[1] = 0
a
Exercises¶
Exercise 1¶
Implement a function ex1(a)
that finds the largest item in the array a
. It should return the largest item and its corresponding position in the array. If there are multiple maximal elements, then the first one will be returned. Assume that the array is not empty. Implement the function in the next cell. Test your implementation with the other one.
# Implement here
using Test
arr = [3,4,7,3,1,7,2]
@test ex1(arr) == (7,3)
Exercise 2¶
Implement a function ex2(f,g)
that takes two functions f(x)
and g(x)
and returns a new function h(x)
representing the sum of f
and g
, i.e., h(x)=f(x)+g(x)
.
# Implement here
h = ex2(sin,cos)
xs = LinRange(0,2π,100)
@test all(x-> h(x) == sin(x)+cos(x), xs)
Exercise 3 (hard)¶
Function mandel
estimates if a given point (x,y)
in the complex plane belongs to the Mandelbrot set.
function mandel(x,y,max_iters)
z = Complex(x,y)
c = z
threshold=2
for n in 1:max_iters
if abs(z)>threshold
return n-1
end
z = z^2 +c
end
max_iters
end
If the value of mandel
is less than max_iters
, the point is provably outside the Mandelbrot set. If mandel
is equal to max_iters
, then the point is provably inside the set. The larger max_iters
, the better the quality of the estimate (the nicer will be your plot).
Plot the value of function mandel
for each pixel in a 2D grid of the box.
$$(-1.7,0.7)\times(-1.2,1.2).$$
Use a grid resolution of at least 1000 points in each direction and max_iters
at least 10. You can increase these values to get nicer plots. To plot the values use function heatmap
from the Julia package GLMakie
. Use LinRange
to divide the horizontal and vertical axes into pixels. See the documentation of these functions for help. GLMakie
is a GPU-accelerated plotting back-end for Julia. It is a large package and it can take some time to install and to generate the first plot. Be patient.
# Implement here
License¶
This notebook is part of the course Programming Large Scale Parallel Systems at Vrije Universiteit Amsterdam and may be used under a CC BY 4.0 license.