t[1] # indexed like tuple# t[1] = 2 # immutablet.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 fieldsstruct ParameterFree value transformation tex_label description end
Two reasons to create structures: - syntactic shortcut (you access the fields with .) - specify the types of the fields
# Type definitionstruct Parameter value ::Float64 transformation ::Function # Function is a type! tex_label::String description::Stringend
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(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# constructorfunctionParameter(value::Float64, tex_label::String) transformation = identity description ="No description available"returnParameter(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:: Float64end
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 pointstruct Duple{T} # T is a parameter to the type Duple x::T y::Tend
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::Tend
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::Send
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 elsestruct PlanarCoordinate{T<:Number} x::T y::Tend
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 functionsfunctionsumofsins_unstable(n::Integer) sum =0:: Integerfor i in1:n sum +=sin(3.4) endreturn sum endfunctionsumofsins_stable(n::Integer) sum =0.0:: Float64for i in1:n sum +=sin(3.4) endreturn sum end# Compile and runsumofsins_unstable(Int(1e5))sumofsins_stable(Int(1e5))
-25554.110202663698
@timesumofsins_unstable(Int(1e5))
0.000176 seconds
-25554.110202663698
@timesumofsins_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_typefunctionprint_type(x::Number)println("$x is a number")end
print_type (generic function with 1 method)
# Define another methodfunctionprint_type(x::String)println("$x is a string")end
print_type (generic function with 2 methods)
# Define yet another methodfunctionprint_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 functionmethods(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.0f(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!
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)
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.