Julia Basics

Author

Pablo Winant

What is Julia

  • developped at MIT on top of opensource technologies
    • linux / git / llvm
  • syntax inspired by Matlab but:
    • more consistent
    • lots of features from high level languages
  • everything is JIT-compiled
    • interpreted vs compiled treadeoff
    • -> very fast
    • most of the base library is written in Julia
  • opensource/free + vibrant community

Some useful links from QuantEcon:

Excellent resources at: julialang - checkout JuliaAcademy, it’s free - ongoing MOOC at MIT

an example of what you shouldn’t do in Matlab

How I learnt: interpreted code is slow, so vectorize your coe.

function stupid_loop(I,J,K)
    t = 0.0
    for i=1:I
        for j=1:J
            for k = 1:K
                t += 1.0
            end        
        end
    end
    return t
end
@time [ stupid_loop(1000,1000,i) for i =1:10]
  0.054532 seconds (37.67 k allocations: 2.477 MiB, 36.74% compilation time)
10-element Vector{Float64}:
 1.0e6
 2.0e6
 3.0e6
 4.0e6
 5.0e6
 6.0e6
 7.0e6
 8.0e6
 9.0e6
 1.0e7
stupid_loop(1000,1000,1000)
1.0e9

Code is translated to LLVM code then to instructions for the processor. Note that processor instructions are shorter than LLVM code.

@code_llvm stupid_loop(10,10,10)
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:1 within `stupid_loop`
define double @julia_stupid_loop_1248(i64 signext %0, i64 signext %1, i64 signext %2) #0 {
top:
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:3 within `stupid_loop`
; ┌ @ range.jl:5 within `Colon`
; │┌ @ range.jl:397 within `UnitRange`
; ││┌ @ range.jl:404 within `unitrange_last`
     %.inv = icmp sgt i64 %0, 0
     %. = select i1 %.inv, i64 %0, i64 0
; └└└
  br i1 %.inv, label %L16.preheader, label %L85

L16.preheader:                                    ; preds = %top
  %.inv26 = icmp sgt i64 %1, 0
  %.24 = select i1 %.inv26, i64 %1, i64 0
  %.inv27 = icmp sgt i64 %2, 0
  %.25 = select i1 %.inv27, i64 %2, i64 0
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:4 within `stupid_loop`
  %3 = select i1 %.inv26, i1 %.inv27, i1 false
  br i1 %3, label %L33.preheader.split.us.us.us, label %L85

L74.us.us:                                        ; preds = %L63.us.us.us
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:9 within `stupid_loop`
; ┌ @ range.jl:891 within `iterate`
; │┌ @ promotion.jl:499 within `==`
    %.not29.us.us = icmp eq i64 %value_phi3.us.us, %.
; │└
   %4 = add nuw i64 %value_phi3.us.us, 1
; └
  br i1 %.not29.us.us, label %L85, label %L33.preheader.split.us.us.us

L33.preheader.split.us.us.us:                     ; preds = %L74.us.us, %L16.preheader
  %value_phi3.us.us = phi i64 [ %4, %L74.us.us ], [ 1, %L16.preheader ]
  %value_phi4.us.us = phi double [ %6, %L74.us.us ], [ 0.000000e+00, %L16.preheader ]
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:5 within `stupid_loop`
  br label %L50.preheader.us.us.us

L50.preheader.us.us.us:                           ; preds = %L63.us.us.us, %L33.preheader.split.us.us.us
  %value_phi8.us.us.us = phi double [ %6, %L63.us.us.us ], [ %value_phi4.us.us, %L33.preheader.split.us.us.us ]
  %value_phi9.us.us.us = phi i64 [ %5, %L63.us.us.us ], [ 1, %L33.preheader.split.us.us.us ]
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:7 within `stupid_loop`
  br label %L50.us.us.us

L63.us.us.us:                                     ; preds = %L50.us.us.us
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:8 within `stupid_loop`
; ┌ @ range.jl:891 within `iterate`
; │┌ @ promotion.jl:499 within `==`
    %.not28.us.us.us = icmp eq i64 %value_phi9.us.us.us, %.24
; │└
   %5 = add nuw i64 %value_phi9.us.us.us, 1
; └
  br i1 %.not28.us.us.us, label %L74.us.us, label %L50.preheader.us.us.us

L50.us.us.us:                                     ; preds = %L50.us.us.us, %L50.preheader.us.us.us
  %value_phi13.us.us.us = phi double [ %6, %L50.us.us.us ], [ %value_phi8.us.us.us, %L50.preheader.us.us.us ]
  %value_phi14.us.us.us = phi i64 [ %7, %L50.us.us.us ], [ 1, %L50.preheader.us.us.us ]
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:6 within `stupid_loop`
; ┌ @ float.jl:408 within `+`
   %6 = fadd double %value_phi13.us.us.us, 1.000000e+00
; └
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:7 within `stupid_loop`
; ┌ @ range.jl:891 within `iterate`
; │┌ @ promotion.jl:499 within `==`
    %.not.us.us.us = icmp eq i64 %value_phi14.us.us.us, %.25
; │└
   %7 = add nuw i64 %value_phi14.us.us.us, 1
; └
  br i1 %.not.us.us.us, label %L63.us.us.us, label %L50.us.us.us

L85:                                              ; preds = %L74.us.us, %L16.preheader, %top
  %value_phi23 = phi double [ 0.000000e+00, %top ], [ %6, %L74.us.us ], [ 0.000000e+00, %L16.preheader ]
;  @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:10 within `stupid_loop`
  ret double %value_phi23
}
@code_native stupid_loop(10,10,10)
    .text
    .file   "stupid_loop"
    .section    .rodata.cst8,"aM",@progbits,8
    .p2align    3                               # -- Begin function julia_stupid_loop_1283
.LCPI0_0:
    .quad   0x3ff0000000000000              # double 1
    .text
    .globl  julia_stupid_loop_1283
    .p2align    4, 0x90
    .type   julia_stupid_loop_1283,@function
julia_stupid_loop_1283:                 # @julia_stupid_loop_1283
; ┌ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:1 within `stupid_loop`
    .cfi_startproc
# %bb.0:                                # %top
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:3 within `stupid_loop`
; │┌ @ range.jl:5 within `Colon`
; ││┌ @ range.jl:397 within `UnitRange`
; │││┌ @ range.jl:404 within `unitrange_last`
    testq   %rdi, %rdi
    vxorpd  %xmm0, %xmm0, %xmm0
; │└└└
    jle .LBB0_9
# %bb.1:                                # %L16.preheader
    testq   %rsi, %rsi
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:4 within `stupid_loop`
    jle .LBB0_9
# %bb.2:                                # %L16.preheader
    testq   %rdx, %rdx
    jle .LBB0_9
# %bb.3:                                # %L33.preheader.split.us.us.us.preheader
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    %rdi, %rax
    movq    %rsi, %rcx
    vxorpd  %xmm0, %xmm0, %xmm0
    sarq    $63, %rax
    sarq    $63, %rcx
    andnq   %rdi, %rax, %r8
    movabsq $.LCPI0_0, %rdi
    andnq   %rsi, %rcx, %rcx
    movq    %rdx, %rsi
    vmovsd  (%rdi), %xmm1                   # xmm1 = mem[0],zero
    sarq    $63, %rsi
    andnq   %rdx, %rsi, %rax
    movl    $1, %esi
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:5 within `stupid_loop`
    popq    %rbp
    .cfi_def_cfa %rsp, 8
    .cfi_restore %rbp
    .p2align    4, 0x90
.LBB0_5:                                # %L33.preheader.split.us.us.us
                                        # =>This Loop Header: Depth=1
                                        #     Child Loop BB0_6 Depth 2
                                        #       Child Loop BB0_7 Depth 3
    movl    $1, %edi
    .p2align    4, 0x90
.LBB0_6:                                # %L50.preheader.us.us.us
                                        #   Parent Loop BB0_5 Depth=1
                                        # =>  This Loop Header: Depth=2
                                        #       Child Loop BB0_7 Depth 3
    movq    %rax, %rdx
    .p2align    4, 0x90
.LBB0_7:                                # %L50.us.us.us
                                        #   Parent Loop BB0_5 Depth=1
                                        #     Parent Loop BB0_6 Depth=2
                                        # =>    This Inner Loop Header: Depth=3
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:6 within `stupid_loop`
; │┌ @ float.jl:408 within `+`
    vaddsd  %xmm1, %xmm0, %xmm0
; │└
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:7 within `stupid_loop`
; │┌ @ range.jl:891 within `iterate`
; ││┌ @ promotion.jl:499 within `==`
    addq    $-1, %rdx
; │└└
    jne .LBB0_7
# %bb.8:                                # %L63.us.us.us
                                        #   in Loop: Header=BB0_6 Depth=2
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:8 within `stupid_loop`
; │┌ @ range.jl:891 within `iterate`
; ││┌ @ promotion.jl:499 within `==`
    cmpq    %rcx, %rdi
; ││└
    leaq    1(%rdi), %rdi
; │└
    jne .LBB0_6
# %bb.4:                                # %L74.us.us
                                        #   in Loop: Header=BB0_5 Depth=1
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:9 within `stupid_loop`
; │┌ @ range.jl:891 within `iterate`
; ││┌ @ promotion.jl:499 within `==`
    cmpq    %r8, %rsi
; ││└
    leaq    1(%rsi), %rsi
; │└
    jne .LBB0_5
.LBB0_9:                                # %L85
; │ @ /home/pablo/Teaching/ensae/mie37/tutorials/1_Julia_Basics.ipynb:10 within `stupid_loop`
    retq
.Lfunc_end0:
    .size   julia_stupid_loop_1283, .Lfunc_end0-julia_stupid_loop_1283
    .cfi_endproc
; └
                                        # -- End function
    .section    ".note.GNU-stack","",@progbits

Syntax Review

Variable assignment

Assignement operator is = (equality is ==, identity is ===)

# Assign the value 10 to the variable x
x = 10
10
x
10
2 == 3
false
# Variable names can have Unicode characters
# To get ϵ in the REPL, type \epsilon<TAB>
a = 20
α = 10
🐳 = 0.1
🦈 = 0.1 * 🐳
σ = 34
ϵ = 1e-4
0.0001

Default semantic is pass-by-reference:

a = [1,2,3,4]
b = a
a[1] = 10
b
4-element Vector{Int64}:
 10
  2
  3
  4

To work on a copy: copy or deepcopy

a = [1,2,3,4]
b = copy(a)
a[1]=10
b
4-element Vector{Int64}:
 1
 2
 3
 4
a .== b
4-element BitVector:
 0
 1
 1
 1
c = b
4-element Vector{Int64}:
 1
 2
 3
 4
b = [1,2,3,4]
4-element Vector{Int64}:
 1
 2
 3
 4
a .== b
4-element BitVector:
 1
 1
 1
 1
c === b
false

Basic types

# for any object `typeof` returns the type

typeof(a)
Vector{Int64} (alias for Array{Int64, 1})
[1,2,3]
3-element Vector{Int64}:
 1
 2
 3
typeof(randn(3,3))  == Array{Float64, 2}
true

Numbers

y = 2 + 2
4
-y
-4
0.34*23
7.82
3/4
0.75
3//4
3//4
3//4 + 2//3
17//12
typeof(3//4 + 2//3)
Rational{Int64}
# Scalar multiplication doesn't require *
3(4 - 2) 
6
x = 4
2*x + 2x^2
40
typeof(x)
sizeof(x)
8
typeof(10)
Int64
(big(100))//big(1000)
1//10
bitstring(10)
"0000000000000000000000000000000000000000000000000000000000001010"

Booleans

Equality

0 == 1
false
2 != 3
true
3 < 4
true
true == false
false

Identity

a = [34, 35]
b = [34, 35]
c = a
c === a
b === a

Boolean operator

true && false
false
true || false
true
!true
false
a  = 2
b = 3

(a > b) && (factorial(100) > 10)
false

Strings

# Strings are written using double quotes
str = "This is a string"
"This is a string"
ch = '🦆' # this is a character
'🦆': Unicode U+1F986 (category So: Symbol, other)
# Strings can also contain Unicode characters
fancy_str = "α is a string"
"α is a string"
n = 10
println("Iteration : ", n)
Iteration : 10
# String interpolation using $
# The expression in parentheses is evaluated and the result is 
# inserted into the string
a = 2+2
"2 + 2 = $(a+1)"
"2 + 2 = 5"
println("It took me $(a) iterations")
It took me 4 iterations
# String concatenation using *
"hello" * "world"
"helloworld"
print("1")
print("2")
print("3")
123
println("1")
println("2")
println("3")
1
2
3
println("hello ", "world")
hello world

Arrays

Julia has one-dimensional arrays. They are also called Vector.

A = [1, 2]
2-element Vector{Int64}:
 1
 2

All elements have the type:

A = [1, 1.4]
2-element Vector{Float64}:
 1.0
 1.4
typeof(A) == Vector{Int64}
false
A''
2-element Vector{Float64}:
 1.0
 1.4

To get the size of an array:

length(A)
2
size(A)
(2,)

Arrays are mutable

A[1] = 10
10
A
2-element Vector{Float64}:
 10.0
  1.4

Julia has one-based indexing: you refer to the first element as 1 (\(\neq\) zero-based indexing in C or Python)

A[2]
1.4

Arrays are mutable and their size can be changed too:

push!(A, 29)
A
4-element Vector{Float64}:
  1.0
  1.4
 29.0
 29.0
A
3-element Array{Float64,1}:
 10.0
  1.4
 29.0
prepend!(A, 28)
5-element Vector{Float64}:
 28.0
  1.0
  1.4
 29.0
 29.0

Two comments: - the push! operation is fast - ! is a julia convention to express the fact that push! mutates its first argument

["a", "b"]
2-element Vector{String}:
 "a"
 "b"
["a", 1]
2-element Vector{Any}:
  "a"
 1

tuples

size(A)  # is a tuple
(5,)
(5,)
(5,)
# you can create tuples with (,,,)
t = (1,2,3,4)
(1, 2, 3, 4)
t
(1, 2, 3, 4)

tuples differ from arrays in two ways: - they are immutable - they can contain non-homogenous objects

t[1]
1
t[1] = 2
MethodError: MethodError: no method matching setindex!(::NTuple{4, Int64}, ::Int64, ::Int64)
typeof((1, "1", [1]))
Tuple{Int64, String, Vector{Int64}}

2d arrays are also called matrices… and can be used for matrix multiplications.

[3 4; 5 6]
2×2 Matrix{Int64}:
 3  4
 5  6
[ [3, 4];; [5, 6]] # concatenate along second dimension
2×2 Matrix{Int64}:
 3  5
 4  6
a1 = [1,2,3,4]
a2 = [1,2,3,4]  .+ 4
[a1 ;; a2]
cat(a1, a2; dims=2)
4×2 Matrix{Int64}:
 1  5
 2  6
 3  7
 4  8
b = [1 0.6 0]
1×3 Array{Float64,2}:
 1.0  0.6  0.0
B = [0.1 0.2 0.3; 4 5 6]

Other ways to construct arrays:

# zero array
t = zeros(2,3)
t[1,2] = 23.2
t
2×3 Matrix{Float64}:
 0.0  23.2  0.0
 0.0   0.0  0.0
# zero array
t = zeros(Int64,2,3)
# t[1,2] = 23.2
t
2×3 Matrix{Int64}:
 0  0  0
 0  0  0
# random array (uniform distribution)
t= rand(3,3)
t
3×3 Matrix{Float64}:
 0.151296  0.390327  0.239194
 0.726286  0.371063  0.133779
 0.037311  0.183624  0.72499
# random array (normal distribution)
t= randn(3,3)
t
3×3 Array{Float64,2}:
 -0.149832     0.973627  -0.407871
 -0.00251947  -1.46936   -0.141511
  0.676479    -0.774655   0.349923

Vectorized operations take a ., even comparisons (pointwise operations)

B = [1 2;3 4]
2×2 Matrix{Int64}:
 1  2
 3  4
B*B
2×2 Matrix{Int64}:
  7  10
 15  22
B .* B
2×2 Matrix{Int64}:
 1   4
 9  16
f(x) = x^2+1
f (generic function with 1 method)
f(43)
1850
# [ f(e) for e in [1,2,3,4,5] ]
f.([1,2,3,4,5])
5-element Vector{Int64}:
  2
  5
 10
 17
 26

Elements are always accessed with square brackets:

B = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6
You get element $B_{ij}$ with `B[i,j]`
B[1,2]
2

You select a whole row/column with :

B[1,:]
3-element Vector{Int64}:
 1
 2
 3
B[:,1]
2-element Vector{Int64}:
 1
 4
B[:,1:2]
2×2 Matrix{Int64}:
 1  2
 4  5
B[:,1:end-1]
2×2 Matrix{Int64}:
 1  2
 4  5

Control flow

Conditions

x = 0
if x<0
    # block
    println("x is negative")
elseif (x > 0) # optional and unlimited
    println("x is positive")
else         # optional
    println("x is zero")
end
x is zero

While

i = 3
while i > 0
    println(i)
    i -= 1 # decrement
end
3
2
1

For loops: your iterate over any iterable object: - range i1:i2 - vector - tuple

# Iterate through ranges of numbers
for i  (1:3)
    println(i)
end
1
2
3
# Iterate through arrays
cities = ["Boston", "New York", "Philadelphia"]
for city  cities
    println(city)
end
Boston
New York
Philadelphia
cities
3-element Vector{String}:
 "Boston"
 "New York"
 "Philadelphia"
states = ["Massachussets", "New York", "Pennsylvania"]
3-element Vector{String}:
 "Massachussets"
 "New York"
 "Pennsylvania"
two_by_two_iterable = zip(cities, states)
zip(["Boston", "New York", "Philadelphia"], ["Massachussets", "New York", "Pennsylvania"])
typeof(two_by_two_iterable)
Base.Iterators.Zip{Tuple{Vector{String}, Vector{String}}}
collect(two_by_two_iterable)
3-element Vector{Tuple{String, String}}:
 ("Boston", "Massachussets")
 ("New York", "New York")
 ("Philadelphia", "Pennsylvania")
[two_by_two_iterable...]
3-element Vector{Tuple{String, String}}:
 ("Boston", "Massachussets")
 ("New York", "New York")
 ("Philadelphia", "Pennsylvania")
# Iterate through arrays of tuples using zip
for kw in zip(cities, states)
    println(kw)
end
("Boston", "Massachussets")
("New York", "New York")
("Philadelphia", "Pennsylvania")
# Iterate through arrays of tuples using zip
for (city, state) in zip(cities, states)
    println("City: $city | State: $state")
end
City: Boston | State: Massachussets
City: New York | State: New York
City: Philadelphia | State: Pennsylvania
# Iterate through arrays and their indices using enumerate
for (i, city) in enumerate(cities)
    println("City $i is $city")
end
City 1 is Boston
City 2 is New York
City 3 is Philadelphia

List comprehensions

[1:10 ...] # unpack operator
10-element Vector{Float64}:
  1.0
  2.0
  3.0
  4.0
  5.0
  6.0
  7.0
  8.0
  9.0
 10.0
[i^2 for i  in 1:10] # collect with comprehension syntax
10-element Vector{Int64}:
   1
   4
   9
  16
  25
  36
  49
  64
  81
 100
[i^2 for i=1:10000000 if mod(i,2)==0] ;
@time sum( [i^2 for i=1:10000000000 if mod(i,2)==0] )
function fun()
    t = 0
    for  i=1:10000000000
        if mod(i,2)==0
            t += i^2
        end
    end
    return t
end
fun (generic function with 1 method)
@time fun()
  2.552283 seconds
-4494074839411110912
gen = (i^2 for i=1:10000000000 if mod(i,2)==0)
Base.Generator{Base.Iterators.Filter{var"#24#26", UnitRange{Int64}}, var"#23#25"}(var"#23#25"(), Base.Iterators.Filter{var"#24#26", UnitRange{Int64}}(var"#24#26"(), 1:10000000000))
@time sum(gen)
 16.349360 seconds (1 allocation: 16 bytes)
-4494074839411110912
## Named Tuples
t = (;a=1,b=2,c=3)
(a = 1, b = 2, c = 3)
t[1] # indexed like tuple
# t[1] = 2 # immutable
t.a # access fields using names
1
model = (;
    α = 0.3,
    β = 0.96
)
(α = 0.3, β = 0.96)
merge(model, (;β=0.9, γ=0.2))
(α = 0.3, β = 0.9, γ = 0.2)
# unpack values from a tuple

α = model[1]
β = model[2]
0.96
# unpack values from a namedtuple

α = model.α
β = model.β
0.96
# namedtuple unpacking

(;α, β) = model
α
0.3
0.3

Data Types and multiple dispatch

Composite types

A composite type is a collection of named fields that can be treated as a single value. They bear a passing resemblance to MATLAB structs.

All fields must be declared ahead of time. The double colon, ::, constrains a field to contain values of a certain type. This is optional for any field.

# Type definition with 4 fields
struct ParameterFree
    value  
    transformation  
    tex_label
    description 
end
pf = ParameterFree("1", x->x^2, "\\sqrt{1+x^2}", ("a",1))
ParameterFree("1", var"#9#10"(), "\\sqrt{1+x^2}", ("a", 1))
pf.value
"1"

Two reasons to create structures: - syntactic shortcut (you access the fields with .) - specify the types of the fields

# Type definition
struct Parameter
    value ::Float64
    transformation ::Function # Function is a type!
    tex_label::String
    description::String
end
p = Parameter("1", x->x^2, "\\sqrt{1+x^2}", ("a",1))
MethodError: Cannot `convert` an object of type String to an object of type Float64
Closest candidates are:
  convert(::Type{T}, ::T) where T<:Number at number.jl:6
  convert(::Type{T}, ::Number) where T<:Number at number.jl:7
  convert(::Type{T}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:250
  ...
p = Parameter(0.43, x->x^2, "\\sqrt{1+x^2}", "This is a description")
Parameter(0.43, var"#13#14"(), "\\sqrt{1+x^2}", "This is a description")
p.value
0.43

When a type with \(n\) fields is defined, a constructor (function that creates an instance of that type) that takes \(n\) ordered arguments is automatically created. Additional constructors can be defined for convenience.

# Creating an instance of the Parameter type using the default
# constructor
β = Parameter(0.9, identity, "\\beta", "Discount rate")
Parameter(0.9, identity, "\\beta", "Discount rate")
function Parameter(value)
    return Parameter(value, x->x, "x", "Anonymous")
end
Parameter
Parameter(0.4)
Parameter(0.4, var"#15#16"(), "x", "Anonymous")
Parameter(value, transformation, tex) = Parameter(value, transformation, tex, "no description")
Parameter
methods( Parameter )
# 4 methods for type constructor:
  • Parameter(value::Float64, transformation::Function, tex_label::String, description::String) in Main at In[100]:3
  • Parameter(value) in Main at In[106]:1
  • Parameter(value, transformation, tex) in Main at In[108]:1
  • Parameter(value, transformation, tex_label, description) in Main at In[100]:3
# Alternative constructors end with an appeal to the default
# constructor
function Parameter(value::Float64, tex_label::String)
    transformation = identity
    description = "No description available"
    return Parameter(value, transformation, tex_label, description)
end

α = Parameter(0.5, "\alpha")
Parameter(0.5, identity, "\alpha", "No description available")

Now the function Parameter has two different methods with different signatures:

methods(Parameter)
# 4 methods for type constructor:
  • Parameter(value::Float64, transformation::Function, tex_label::String, description::String) in Main at In[1]:3
  • Parameter(value::Float64, tex_label::String) in Main at In[8]:4
  • Parameter(value, transformation, tex) in Main at In[5]:1
  • Parameter(value, transformation, tex_label, description) in Main at In[1]:3

We have seen that a function can have several implementations, called methods, for different number of arguments, or for different types of arguments.

fun(x::Int64, y::Int64) = x^3 + y
fun (generic function with 1 method)
fun(x::Float64, y::Int64) = x/2 + y
fun (generic function with 2 methods)
fun(2, 2)
10
fun(2.0, 2)
3.0
α.tex_label
# Access a particular field using .
α.value
0.5
# Fields are modifiable and can be assigned to, like 
# ordinary variables
α.value = 0.75

Mutable vs non mutable types

by default structures in Julia are non-mutable

p.value = 3.0
setfield! immutable struct of type Parameter cannot be changed
mutable struct Params
    x:: Float64
    y:: Float64
end
pos = Params(0.4, 0.2)
Params(0.4, 0.2)
pos.x = 0.5
0.5

Parameterized Types

Parameterized types are data types that are defined to handle values identically regardless of the type of those values.

Arrays are a familiar example. An Array{T,1} is a one-dimensional array filled with objects of any type T (e.g. Float64, String).

# Defining a parametric point
struct Duple{T} # T is a parameter to the type Duple
    x::T
    y::T
end
Duple(3, 3)
Duple{Int64}(3, 3)
Duple(3, -1.0)
MethodError: no method matching Duple(::Int64, ::Float64)
Closest candidates are:
  Duple(::T, ::T) where T at In[127]:3
struct Truple{T}
    x::Duple{T}
    z::T
end

This single declaration defines an unlimited number of new types: Duple{String}, Duple{Float64}, etc. are all immediately usable.

sizeof(3.0)
8
sizeof( Duple(3.0, -15.0) )
16
# What happens here?
Duple(1.5, 3)
struct Truple3{T,S}
    x::Tuple{T,S}
    z::S
end

We can also restrict the type parameter T:

typeof("S") <: Number
false
typeof(4) <: Number
true
# T can be any subtype of Number, but nothing else
struct PlanarCoordinate{T<:Number}
    x::T
    y::T
end
PlanarCoordinate("4th Ave", "14th St")
MethodError: MethodError: no method matching PlanarCoordinate(::String, ::String)
PlanarCoordinate(2//3, 8//9)
PlanarCoordinate{Rational{Int64}}(2//3, 8//9)

Arrays are an exemple of mutable, parameterized types

Why Use Types?

You can write all your code without thinking about types at all. If you do this, however, you’ll be missing out on some of the biggest benefits of using Julia.

If you understand types, you can:

  • Write faster code
  • Write expressive, clear, and well-structured programs (keep this in mind when we talk about functions)
  • Reason more clearly about how your code works

Even if you only use built-in functions and types, your code still takes advantage of Julia’s type system. That’s why it’s important to understand what types are and how to use them.

# Example: writing type-stable functions
function sumofsins_unstable(n::Integer)  
    sum = 0:: Integer
    for i in 1:n  
        sum += sin(3.4)  
    end  
    return sum 
end  

function sumofsins_stable(n::Integer)  
    sum = 0.0 :: Float64
    for i in 1:n  
        sum += sin(3.4)  
    end  
    return sum 
end

# Compile and run
sumofsins_unstable(Int(1e5))
sumofsins_stable(Int(1e5))
-25554.110202663698
@time sumofsins_unstable(Int(1e5))
  0.000176 seconds
-25554.110202663698
@time sumofsins_stable(Int(1e5))
  0.000168 seconds
-25554.110202663698

In sumofsins_stable, the compiler is guaranteed that sum is of type Float64 throughout; therefore, it saves time and memory. On the other hand, in sumofsins_unstable, the compiler must check the type of sum at each iteration of the loop. Let’s look at the LLVM intermediate representation.

Multiple Dispatch

So far we have defined functions over argument lists of any type. Methods allow us to define functions “piecewise”. For any set of input arguments, we can define a method, a definition of one possible behavior for a function.

# Define one method of the function print_type
function print_type(x::Number)
    println("$x is a number")
end
print_type (generic function with 1 method)
# Define another method
function print_type(x::String)
    println("$x is a string")
end
print_type (generic function with 2 methods)
# Define yet another method
function print_type(x::Number, y::Number)
    println("$x and $y are both numbers")
end
print_type (generic function with 3 methods)
# See all methods for a given function
methods(print_type)
# 3 methods for generic function print_type:
  • print_type(x::String) in Main at In[53]:3
  • print_type(x::Number) in Main at In[51]:3
  • print_type(x::Number, y::Number) in Main at In[54]:3

Julia uses multiple dispatch to decide which method of a function to execute when a function is applied. In particular, Julia compares the types of all arguments to the signatures of the function’s methods in order to choose the applicable one, not just the first (hence “multiple”).

print_type(5)
5 is a number
print_type("foo")
foo is a string
print_type([1, 2, 3])
MethodError: MethodError: no method matching print_type(::Array{Int64,1})
Closest candidates are:
  print_type(!Matched::String) at In[53]:3
  print_type(!Matched::Number) at In[51]:3
  print_type(!Matched::Number, !Matched::Number) at In[54]:3

Other types of functions

Julia supports a short function definition for one-liners

f(x::Float64) = x^2.0
f(x::Int64) = x^3

As well as a special syntax for anonymous functions

u->u^2
map(u->u^2, [1,2,3,4])

Keyword arguments and optional arguments

f(a,b,c=true; algo="newton")
UndefVarError: UndefVarError: f not defined

Packing/unpacking

t = (1,2,4)
(1, 2, 4)
a,b,c = t
(1, 2, 4)
[(1:10)...]
10-element Array{Int64,1}:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
cat([4,3], [0,1]; dims=1)
4-element Array{Int64,1}:
 4
 3
 0
 1
l = [[4,3], [0,1], [0, 0], [1, 1]]
# how do I concatenate it ?

cat(l...; dims=1) ### see python's f(*s)
8-element Array{Int64,1}:
 4
 3
 0
 1
 0
 0
 1
 1

Writing Julian Code

As we’ve seen, you can use Julia just like you use MATLAB and get faster code. However, to write faster and better code, attempt to write in a “Julian” manner:

  • Define composite types as logically needed
  • Write type-stable functions for best performance
  • Take advantage of multiple dispatch to write code that looks like math
  • Add methods to existing functions

Just-in-Time Compilation

How is Julia so fast? Julia is just-in-time (JIT) compiled, which means (according to this StackExchange answer):

A JIT compiler runs after the program has started and compiles the code (usually bytecode or some kind of VM instructions) on the fly (or just-in-time, as it’s called) into a form that’s usually faster, typically the host CPU’s native instruction set. A JIT has access to dynamic runtime information whereas a standard compiler doesn’t and can make better optimizations like inlining functions that are used frequently.

This is in contrast to a traditional compiler that compiles all the code to machine language before the program is first run.

In particular, Julia uses type information at runtime to optimize how your code is compiled. This is why writing type-stable code makes such a difference in speed!

Additional Exercises

Taken from QuantEcon’s Julia Essentials and Vectors, Arrays, and Matrices lectures.

  1. Consider the polynomial \[p(x) = \sum_{i=0}^n a_0 x^0\] Using enumerate, write a function p such that p(x, coeff) computes the value of the polynomial with coefficients coeff evaluated at x.
ppp (generic function with 1 method)
  1. Write a function solve_discrete_lyapunov that solves the discrete Lyapunov equation \[S = ASA' + \Sigma \Sigma'\] using the iterative procedure \[S_0 = \Sigma \Sigma'\] \[S_{t+1} = A S_t A' + \Sigma \Sigma'\] taking in as arguments the \(n \times n\) matrix \(A\), the \(n \times k\) matrix \(\Sigma\), and a number of iterations.