Reference: The Julia Documentation, The Julia–Matlab–Python Cheatsheet
This lecture gives an overview of Julia.
In these notes we focus on the aspects of Julia and computing that are essential to numerical computing:
Copyright By PowCoder代写 加微信 powcoder
Integers: We discuss briefly how to create and manipulate integers, and how to see the underlying bit representation.
Strings and parsing: We discuss how to create and manipulate strings and characters, and how we can convert a string
of 0’s and 1’s to an integer or other type.
Vectors and matrices: We discuss how to build and manipulate vectors and matrices (which are both types of arrays).
Later lectures will discuss linear algebra.
Types: In Julia everything has a type, which plays a similar role to classes in Python. Here we
discuss how to make new types, for example, a complex number in radial format.
Loops and branches: We discuss if, for and while, which work similar to Python.
Functions: We discuss the construction of named and anonymous functions.
Julia allows overloading functions for different types, for example, we can overload * for
our radial complex type.
Modules, Packages, and Plotting: We discuss how to load external packages, in particular, for plotting.
Note some subsections are labeled advanced, these are non-examinable and will not be necessary in
problem sheets or exams.
1. Integers¶
Julia uses a math-like syntax for manipulating integers:
1 + 1 # Addition
2 * 3 # Multiplication
2 / 3 # Division
0.6666666666666666
x = 5; # semicolon is optional but supresses output if used in the last line
x^2 # Powers
In Julia everything has a type. This is similar in spirit to
a class in Python, but much more lightweight.
An integer defaults to a type Int,
which is either 32-bit (Int32) or 64-bit (Int64) depending
on the processor of the machine. There are also 8-bit (Int8), 16-bit (Int16),
and 128-bit (Int128) integer types, which
we can construct by converting an Int, e.g. Int8(3).
These are all “primitive types”, instances of the type are stored in memory as
a fixed length sequence of bits.
We can find the type of a variable as follows:
For a primitive type we can see the bits using the function bitstring:
bitstring(Int8(1))
“00000001”
Negative numbers may be surprising:
bitstring(-Int8(1))
“11111111”
This is explained in detail in Chapter Numbers
There are other primitive integer types: UInt8, UInt16, UInt32, and UInt64 are unsigned integers,
e.g., we do not interpret
the number as negative if the first bit is 1. As they tend to be used to represent
bit sequences they are displayed in hexadecimal, that is base-16, using digits 0-9a-c,
e.g., $12 = (c)_{16}$:
UInt16(12)
A non-primitive type is BigInt which allows arbitrary length
2. Strings and parsing¶
We have seen that bitstring returns a string of bits.
Strings can be created with quotation marks
str = “hello world 😀”
“hello world 😀”
We can access characters of a string with brackets:
str[1], str[13]
(‘h’, ‘😀’)
Each character is a primitive type, in this case using 32 bits/4 bytes:
typeof(str[6]), length(bitstring(str[6]))
(Char, 32)
Strings are not primitive types, but rather point to the start of a sequence
of Chars in memory. In this case, there are $32*13=416$ bits/52
bytes in memory.
Strings are immutable: once created they cannot be changed.
But a new string can be created that modifies an existing string.
The simplest example is *, which concatenates two strings:
“hi” * “bye”
(Why *? Because concatenation is non-commutive.)
We can combine this with indexing to, for example, create a new string
with a different last character:
str[1:end-1] * “😂”
“hello world 😂”
Parsing strings¶
We can use the command parse to turn a string into an integer:
parse(Int, “123”)
We can specify base 2 as an optional argument:
parse(Int, “-101”; base=2)
If we are specifying
bits its safer to parse as an UInt32, otherwise the first bit
is not recognised as a sign:
bts = “11110000100111111001100110001010”
x = parse(UInt32, bts; base=2)
0xf09f998a
The function reinterpret allows us to reinterpret the resulting
sequence of 32 bits as a different type. For example, we can reinterpret
as an Int32 in which case the first bit is taken to be the sign bit
and we get a negative number:
reinterpret(Int32, x)
-257975926
We can also reinterpret as a Char:
reinterpret(Char, x)
‘🙊’: Unicode U+1F64A (category So: Symbol, other)
We will use parse and reinterpret as it allows one to easily manipulate bits.
This is not actually how one should
do it as it is slow.
Bitwise operations (advanced)¶
In practice, one should manipulate bits using bitwise operations.
These will not be required in this course and are not examinable, but
are valuable to know if you have a career involving high performance computing.
The p << k shifts the bits of p to the left k times inserting zeros,
while p >> k shifts to the right:
println(bitstring(23));
println(bitstring(23 << 2));
println(bitstring(23 >> 2));
0000000000000000000000000000000000000000000000000000000000010111
0000000000000000000000000000000000000000000000000000000001011100
0000000000000000000000000000000000000000000000000000000000000101
The operations &, | and ⊻ do bitwise and, or, and xor.
3. Vectors, Matrices, and Arrays¶
We can create a vector using brackets:
v = [11, 24, 32]
3-element Vector{Int64}:
Like a string, elements are accessed via brackets. Julia
uses 1-based indexing (like Matlab and Mathematica, unlike
Python and C which use 0-based indexing):
v[1], v[3]
Accessing outside the range gives an error:
BoundsError: attempt to access 3-element Vector{Int64} at index [4]
Stacktrace:
[1] getindex(A::Vector{Int64}, i1::Int64)
@ Base ./array.jl:861
[2] top-level scope
@ In[22]:1
@ ./boot.jl:373 [inlined]
[4] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1196
Vectors can be made with different types, for example,
here is a vector of three 8-bit integers:
v = [Int8(11), Int8(24), Int8(32)]
3-element Vector{Int8}:
Just like strings, Vectors are not primitive types,
but rather point to the start of sequence of bits in memory
that are interpreted in the corresponding type.
In this last case, there are $3*8=24$ bits/3 bytes in memory.
The easiest way to create a vector is to use zeros to create a zero Vector
and then modify its entries:
v = zeros(Int, 5)
5-element Vector{Int64}:
Note: we can’t assign a non-integer floating point number to an integer vector:
v[2] = 3.5
InexactError: Int64(3.5)
Stacktrace:
@ ./float.jl:812 [inlined]
[2] convert
@ ./number.jl:7 [inlined]
[3] setindex!(A::Vector{Int64}, x::Float64, i1::Int64)
@ Base ./array.jl:903
[4] top-level scope
@ In[25]:1
@ ./boot.jl:373 [inlined]
[6] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1196
We can also create vectors with ones (a vector of all ones), rand (a vector of random numbers between 0 and 1)
and randn (a vector of samples of normal distributed quasi-random numbers).
When we create a vector whose entries are of different types, they are mapped to a type that can represent every entry.
For example, here we input a list of one Int32 followed by three Int64s, which are automatically converted to
all be Int64:
[Int32(1), 2, 3, 4]
4-element Vector{Int64}:
In the event that the types cannot automatically be converted, it defaults to an Any vector, which
is similar to a Python list.
This is bad performancewise as it does not know how many bits each element will need, so should be avoided.
[1.0, 1, “1”]
3-element Vector{Any}:
We can also specify the type of the Vector explicitly by writing the desired type before the first bracket:
Int32[1, 2, 3]
3-element Vector{Int32}:
We can also create an array using comprehensions:
[k^2 for k = 1:5]
5-element Vector{Int64}:
Matrices are created similar to vectors, but by specifying two dimensions instead of one.
Again, the simplest way is to use zeros to create a matrix of all zeros:
zeros(Int, 5, 5) # creates a 5×5 matrix of Int zeros
5×5 Matrix{Int64}:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
We can also create matrices by hand. Here, spaces delimit the columns and semicolons delimit the rows:
A = [1 2; 3 4; 5 6]
3×2 Matrix{Int64}:
We can also create matrices using brackets, a formula, and a for command:
[k^2+j for k=1:5, j=1:5]
5×5 Matrix{Int64}:
2 3 4 5 6
5 6 7 8 9
10 11 12 13 14
17 18 19 20 21
26 27 28 29 30
Matrices are really vectors in disguise. They are still stored in memory in a consecutive sequence of bits.
We can see the underlying vector using the vec command:
6-element Vector{Int64}:
The only difference between matrices and vectors from the computers perspective is that they have a size which
changes the interpretation of whats stored in memory:
Matrices can be manipulated easily on a computer. We can multiply a matrix times vector:
x = [8; 9]
3-element Vector{Int64}:
or a matrix times matrix:
A * [4 5; 6 7]
3×2 Matrix{Int64}:
If you use .*, it does entrywise multiplication:
[1 2; 3 4] .* [4 5; 6 7]
2×2 Matrix{Int64}:
We can take the transpose of a real vector as follows:
a = [1, 2, 3]
1×3 adjoint(::Vector{Int64}) with eltype Int64:
Note for complex-valued vectors this is the conjugate-transpose,
and so one may need to use transpose(a). Both a’ and transpose(a)
should be thought of as “dual-vectors”, and so multiplcation with a
transposed vector with a normal vector gives a constant:
b = [4, 5, 6]
One important note: a vector is not the same as an n x 1 matrix, and a transposed
vector is not the same as a 1 \times n matrix.
Accessing and altering subsections of arrays¶
We will use the following notation to get at the columns and rows of matrices:
A[a:b,k] # returns the a-th through b-th rows of the k-th column of A as a Vector of length (b-a+1)
A[k,a:b] # returns the a-th through b-th columns of the k-th row of A as a Vector of length (b-a+1)
A[:,k] # returns all rows of the k-th column of A as a Vector of length size(A,1)
A[k,:] # returns all columns of the k-th row of A as a Vector of length size(A,2)
A[a:b,c:d] # returns the a-th through b-th rows and c-th through d-th columns of A
# as a (b-a+1) x (d-c+1) Matrix
The ranges a:b and c:d can be replaced by any AbstractVector{Int}. For example:
A = [1 2 3; 4 5 6; 7 8 9; 10 11 12]
A[[1,3,4],2] # returns the 1st, 3rd and 4th rows of the 2nd column of A
3-element Vector{Int64}:
Exercise Can you guess what A[2,[1,3,4]] returns, using the definition of A as above?
What about A[1:2,[1,3]]? And A[1,B[1:2,1]]? And vec(A[1,B[1:2,1]])?
We can also use this notation to modify entries of the matrix. For example, we can set the 1:2 x 2:3 subblock of A to [1 2; 3 4] as follows:
A[1:2,2:3] = [1 2; 3 4]
4×3 Matrix{Int64}:
10 11 12
Broadcasting¶
It often is necessary to apply a function to every entry of a vector.
By adding . to the end of a function we “broadcast” the function over
x = [1,2,3]
cos.(x) # equivalent to [cos(1), cos(2), cos(3)]
3-element Vector{Float64}:
0.5403023058681398
-0.4161468365471424
-0.9899924966004454
Broadcasting has some interesting behaviour for matrices.
If one dimension of a matrix (or vector) is 1, it automatically
repeats the matrix (or vector) to match the size of another example.
[1,2,3] .* [4,5]’
3×2 Matrix{Int64}:
Since size([1,2,3],2) == 1 it repeats the same vector to match the size
size([4,5]’,2) == 2. Similarly, [4,5]’ is repeated 3 times. So the
above is equivalent to:
[1 1; 2 2; 3 3] .* [4 5; 4 5; 4 5]
3×2 Matrix{Int64}:
Note we can also use broadcasting with our own functions (construction discussed later):
f = (x,y) -> cos(x + 2y)
f.([1,2,3], [4,5]’)
3×2 Matrix{Float64}:
-0.91113 0.0044257
-0.839072 0.843854
0.0044257 0.907447
We have already seen that we can represent a range of integers via a:b. Note we can
convert it to a Vector as follows:
Vector(2:6)
5-element Vector{Int64}:
We can also specify a step:
Vector(2:2:6), Vector(6:-1:2)
([2, 4, 6], [6, 5, 4, 3, 2])
Finally, the range function gives more functionality, for example, we can create 4 evenly
spaced points between -1 and 1:
Vector(range(-1, 1; length=4))
4-element Vector{Float64}:
-0.3333333333333333
0.3333333333333333
Note that Vector is mutable but a range is not:
r[2] = 3 # Not allowed
setindex! not defined for UnitRange{Int64}
Stacktrace:
[1] error(::String, ::Type)
@ Base ./error.jl:42
[2] error_if_canonical_setindex(#unused#::IndexLinear, A::UnitRange{Int64}, #unused#::Int64)
@ Base ./abstractarray.jl:1323
[3] setindex!(A::UnitRange{Int64}, v::Int64, I::Int64)
@ Base ./abstractarray.jl:1314
[4] top-level scope
@ In[49]:2
@ ./boot.jl:373 [inlined]
[6] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1196
Julia has two different kinds of types: primitive types (like Int64, Int32, UInt32 and Char) and composite types.
Here is an example of an in-built composite type representing complex numbers, for example,
$z = 1+2{\rm i}$:
z = 1 + 2im
Complex{Int64}
A complex number consists of two fields: a real part (denoted re)
and an imaginary part (denoted im).
Fields of a type can be accessed using the . notation:
z.re, z.im
We can make our own types. Let’s make a type to represent complex numbers in the format
$$z=r {\rm exp}({\rm i}\theta)$$
That is, we want to create a type with two fields: r and θ.
This is done using the struct syntax,
followed by a list of names for the fields,
and finally the keyword end.
struct RadialComplex
z = RadialComplex(1,0.1)
RadialComplex(1, 0.1)
We can access the fields using .:
Note that the fields are immutable: we can create
a new RadialComplex but we cannot modify an existing one.
To make a mutable type we use the command mutable struct:
mutable struct MutableRadialComplex
z = MutableRadialComplex(1,2)
MutableRadialComplex(2, 3)
Abstract types¶
Every type is a sub-type of an abstract type, which can never be instantiated on its own.
For example, every integer and floating point number is a real number.
Therefore, there is an abstract type Real, which encapsulates many other types,
including Float64, Float32, Int64 and Int32.
We can test if type T is part of an abstract type V using the sytax T <: V: Float64 <: Real, Float32 <: Real, Int64 <: Real (true, true, true) Every type has one and only one super type, which is always an abstract type. The function supertype applied to a type returns its super type: supertype(Int32) # returns Signed, which represents all signed integers. supertype(Float32) # returns `AbstractFloat`, which is a subtype of `Real` AbstractFloat An abstract type also has a super type: supertype(Real) Type annotation and templating¶ The types RadialComplex and MutableRadialComplex won't be efficient as we have not told the compiler the type of r and θ. For the purposes of this module, this is fine as we are not focussing on high performance computing. However, it may be of interest how to rectify this. We can impose a type on the field name with ::: struct FastRadialComplex r::Float64 θ::Float64 z = FastRadialComplex(1,0.1) (1.0, 0.1) In this case z is stored using precisely 128-bits. Sometimes we want to support multiple types. For example, we may wish to support 32-bit floats. This can be done as follows: struct TemplatedRadialComplex{T} z = TemplatedRadialComplex(1f0,0.1f0) # f0 creates a `Float32` TemplatedRadialComplex{Float32}(1.0f0, 0.1f0) This is stored in precisely 64-bits. Relationship with C structs, heap and stack (advanced)¶ For those familiar with C, a struct in Julia whose fields are primitive types or composite types built from primitive types, is exactly equivalent to a struct C, and can in fact be passed to C functions without any performance cost. Behind the scenes Julia uses the LLVM compiler and so C and Julia can be freely mixed. Another thing to note is that there are two types of memory: the stack and the heap. The stack has fixed memory length and is much faster as it avoids dynamic allocation and deallocation of memory. So an instance of a type with a known fixed length (like FastRadialComplex) will typically be in the stack and be much faster than an instance of a type with unknown or variable length (like RadialComplex or Vector), which will be on the heap. For stack-allocated instances, the compiler may even go a step further and compile a function so that an instance of a type only lives on the cache or even in registers. 5. Loops and branches¶ For loops work essentially the same as in Python. The one caveat is to re 程序代写 CS代考 加微信: powcoder QQ: 1823890830 Email: powcoder@163.com