Lab 01: Introduction to Julia
This lab should get everyone up to speed in the basics of Julia's installation, syntax and basic coding. For more detailed introduction you can check out Lectures 1-3 of the bachelor course.
Testing Julia installation (custom setup)
In order to proceed further let's run a simple script to see, that the setup described in chapter Installation is working properly. After spawning a terminal/cmdline run this command:
julia ./test_setup.jl
The script does the following
- "Tests" if Julia is added to path and can be run with
julia
command from anywhere - Prints Julia version info
- Checks Julia version.
- Checks git configuration (name + email)
- Creates an environment configuration files
- Installs a basic pkg called BenchmarkTools, which we will use for benchmarking a simple function later in the labs.
There are some quality of life improvements over long term support versions of Julia and thus throughout this course we will use the latest stable release of Julia 1.6.x.
Polynomial evaluation example
Let's consider a common mathematical example for evaluation of nth-degree polynomial
\[f(x) = a_{n}x^{n} + a_{n-1}x^{n-1} + \dots + a_{0}x^{0},\]
where $x \in \mathbb{R}$ and $\vec{a} \in \mathbb{R}^{n+1}$.
The simplest way of writing this in a generic fashion is realizing that essentially the function $f$ is really implicitly containing argument $\vec{a}$, i.e. $f \equiv f(\vec{a}, x)$, yielding the following Julia code
function polynomial(a, x)
accumulator = 0
for i in length(a):-1:1
accumulator += x^(i-1) * a[i] # ! 1-based indexing for arrays
end
return accumulator
end
Evaluate the code of the function called polynomial
in Julia REPL and evaluate the function itself with the following arguments.
a = [-19, 7, -4, 6] # list coefficients a from a^0 to a^n
x = 3 # point of evaluation
Details
The simplest way is to just copy&paste into an already running terminal manually. As opposed to the default Python REPL, Julia can deal with the blocks of code and different indentation much better without installation of an ipython
-like REPL. There are ways to make this much easier in different text editors/IDEs:
VSCode
- when using Julia extension is installed and.jl
file is opened,Ctrl/Cmd+Enter
will spawn Julia REPLSublime Text
-Ctrl/Cmd+Enter
withSend Code
pkg (works well with Linux terminal or tmux, support for Windows is poor)Vim
- there is a Julia language plugin, which can be combine with vimcmdline to gain similar functionality
Either way, you should see the following:
julia> function polynomial(a, x) accumulator = 0 for i in length(a):-1:1 accumulator += x^(i-1) * a[i] # ! 1-based indexing for arrays end return accumulator end
polynomial (generic function with 1 method)
Similarly we enter the arguments of the function a
and x
:
julia> a = [-19, 7, -4, 6]
4-element Vector{Int64}: -19 7 -4 6
julia> x = 3
3
Function call intuitively takes the name of the function with round brackets as arguments, i.e. works in the same way as majority of programming languages. The result is printed unless a ;
is added at the end of the statement.
julia> polynomial(a, x) # function call
128
Thanks to the high level nature of Julia language it is often the case that examples written in pseudocode are almost directly rewritable into the language itself without major changes and the code can be thus interpreted easily.
Due to the existence of the end
keyword, indentation is not necessary as opposed to other languages such as Python, however it is strongly recommended to use it, see style guide.
Though there are libraries/IDEs that allow us to step through Julia code (Debugger.jl
link and VSCode
link), here we will explore the code interactively in REPL by evaluating pieces of code separately.
Basic types, assignments and variables
When defining a variable through an assignment we get the representation of the right side, again this is different from the default behavior in Python, where the output of assignments a = [-19, 7, -4, 6]
or x = 3
, prints nothing. Internally Julia returns the result of the display
function.
julia> a = [-19, 7, -4, 6]
4-element Vector{Int64}: -19 7 -4 6
julia> display(a) # should return the same thing as the line above
As you can see, the string that is being displayed contains information about the contents of a variable along with it's type in this case this is a Vector/Array
of Int
types. If the output of display
is insufficient the type of variable can be checked with the typeof
function:
julia> typeof(a)
Vector{Int64} (alias for Array{Int64, 1})
Additionally for collection/iterable types such as Vector
there is also the eltype
function, which returns the type of elements in the collection.
julia> eltype(a)
Int64
In most cases variables store just a reference to a place in memory either stack/heap (exceptions are primitive types such as Int
, Float
) and therefore creating an array a
, "storing" the reference in b
with an assignment and changing elements of b
, e.g. b[1] = 2
, changes also the values in a
.
Create variables x
and accumulator
, storing floating point 3.0
and integer value 0
respectively. Check the type of variables using typeof
function.
Solution
julia> x = 3.0
3.0
julia> accumulator = 0
0
julia> typeof(x), typeof(accumulator)
(Float64, Int64)
For cycles and ranges
Moving further into the polynomial function we encounter the definition of a for cycle, with the de facto standard syntax
for iteration_variable in iterator
# do something
end
As an example of iterator we have used an instance of a range type
julia> r = length(a):-1:1
4:-1:1
julia> typeof(r)
StepRange{Int64, Int64}
As opposed to Python, ranges in Julia are inclusive, i.e. they contain number from start to end - in this case running from 4
to 1
with negative step -1
, thus counting down. This can be checked with the collect
and/or length
functions.
julia> collect(r)
4-element Vector{Int64}: 4 3 2 1
julia> length(r)
4
Create variable c
containing an array of even numbers from 2
to 42
. Furthermore create variable d
that is different from c
only at the 7th position, which will contain 13
.
HINT: Use collect
function for creation of c
and copy
for making a copy of c
.
Solution
julia> c = collect(2:2:42)
21-element Vector{Int64}: 2 4 6 8 10 12 14 16 18 20 ⋮ 26 28 30 32 34 36 38 40 42
julia> d = copy(c)
21-element Vector{Int64}: 2 4 6 8 10 12 14 16 18 20 ⋮ 26 28 30 32 34 36 38 40 42
julia> d[7] = 13
13
julia> d
21-element Vector{Int64}: 2 4 6 8 10 12 13 16 18 20 ⋮ 26 28 30 32 34 36 38 40 42
Functions and operators
Let us now move from the function body to the function definition itself. From the picture at the top of the page, we can infer the general syntax for function definition:
function function_name(arguments)
# do stuff with arguments and define output value `something`
return something
end
The return keyword can be omitted, if the last line being evaluated contains the result.
By creating the function polynomial
we have defined a variable polynomial
, that from now on always refers to a function and cannot be reassigned to a different type, like for example Int
.
julia> polynomial = 42
ERROR: invalid redefinition of constant Main.polynomial
This is caused by the fact that each function defines essentially a new type, the same like Int ~ Int64
or Vector{Int}
.
julia> typeof(polynomial)
typeof(Main.polynomial) (singleton type of function polynomial, subtype of Function)
You can check that it is a subtype of the Function
abstract type, with the subtyping operator <:
julia> typeof(polynomial) <: Function
true
These concepts will be expanded further in the type system lecture, however for now note that this construction is quite useful for example if we wanted to create derivative rules for our function derivativeof(::typeof(polynomial), ...)
.
Looking at mathematical operators +
, *
, we can see that in Julia they are also standalone functions.
julia> +
+ (generic function with 294 methods)
julia> *
* (generic function with 389 methods)
The main difference from our polynomial
function is that there are multiple methods, for each of these functions. Each one of the methods corresponds to a specific combination of arguments, for which the function can be specialized to using multiple dispatch. You can see the list by calling a methods
function:
julia> methods(+)
# 190 methods for generic function "+":
[1] +(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at
int.jl:87
[2] +(c::Union{UInt16, UInt32, UInt64, UInt8}, x::BigInt) in Base.GMP at gmp.jl:528
[3] +(c::Union{Int16, Int32, Int64, Int8}, x::BigInt) in Base.GMP at gmp.jl:534
...
One other notable difference is that these functions allow using both infix and postfix notation a + b
and +(a,b)
, which is a specialty of elementary functions such as arithmetic operators or set operation such as ∩, ∪, ∈
.
The functionality of methods
is complemented with the reverse lookup methodswith
, which for a given type returns a list of methods that can be called with it as an argument.
julia> methodswith(Int)
[1] +(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:87
[2] +(c::Union{Int16, Int32, Int64, Int8}, x::BigInt) in Base.GMP at gmp.jl:534
[3] +(c::Union{Int16, Int32, Int64, Int8}, x::BigFloat) in Base.MPFR at mpfr.jl:384
[4] +(x::BigFloat, c::Union{Int16, Int32, Int64, Int8}) in Base.MPFR at mpfr.jl:379
[5] +(x::BigInt, c::Union{Int16, Int32, Int64, Int8}) in Base.GMP at gmp.jl:533
...
Define function called addone
with one argument, that adds 1
to the argument.
Solution
julia> function addone(x) x + 1 end
addone (generic function with 1 method)
julia> addone(1) == 2
true
Calling for help
In order to better understand some keywords we have encountered so far, we can ask for help in the Julia's REPL itself with the built-in help terminal. Accessing help terminal can be achieved by writing ?
with a query keyword after. This searches documentation of all the available source code to find the corresponding keyword. The simplest way to create documentation, that can be accessed in this way, is using so called docstring
s, which are multiline strings written above function or type definition.
"""
polynomial(a, x)
Returns value of a polynomial with coefficients `a` at point `x`.
"""
function polynomial(a, x)
# function body
end
More on this in lecture 4 about pkg development.
Lookup docstring
for the basic functions that we have introduced in the previous exercises: typeof
, eltype
, length
, collect
, copy
, methods
and methodswith
.
BONUS: Try it with others, for example with the subtyping operator <:
.
Solution
Example docstring for typeof
function.
typeof(x)
Get the concrete type of x.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> a = 1//2;
julia> typeof(a)
Rational{Int64}
julia> M = [1 2; 3.5 4];
julia> typeof(M)
Matrix{Float64} (alias for Array{Float64, 2})
Testing waters
As the arguments of the polynomial
functions are untyped, i.e. they do not specify the allowed types like for example polynomial(a, x::Number)
does, the following exercise explores which arguments the function accepts, while giving expected result.
Choose one of the variables af
to ac
representing polynomial coefficients and try to evaluate it with the polynomial
function at point x=3
as before. Lookup the type of coefficient collection variable itself with typeof
and the items in the collection with eltype
. In this case we allow you to consult your solution with the expandable solution bellow to find out more information about a particular example.
af = [-19.0, 7.0, -4.0, 6.0]
at = (-19, 7, -4, 6)
ant = (a₀ = -19, a₁ = 7, a₂ = -4, a₃ = 6)
a2d = [-19 -4; 7 6]
ac = [2i^2 + 1 for i in -2:1]
Solution
julia> typeof(af), eltype(af)
(Vector{Float64}, Float64)
julia> polynomial(af, x)
128.0
As opposed to the basic definition of a
type the array is filled with Float64
types and the resulting value gets promoted as well to the Float64
.
julia> typeof(at), eltype(at)
(NTuple{4, Int64}, Int64)
julia> polynomial(at, x)
128.0
With round brackets over a fixed length vector we get the Tuple
type, which is so called immutable "array" of a fixed size (its elements cannot be changed, unless initialized from scratch). Each element can be of a different type, but here we have only one and thus the Tuple
is aliased into NTuple
. There are some performance benefits for using immutable structure, which will be discussed later.
Defining key=value
pairs inside round brackets creates a structure called NamedTuple
, which has the same properties as Tuple
and furthermore its elements can be conveniently accessed by dot syntax, e.g. ant.a₀
.
julia> typeof(ant), eltype(ant)
(@NamedTuple{a₀::Int64, a₁::Int64, a₂::Int64, a₃::Int64}, Int64)
julia> polynomial(ant, x)
128.0
Defining a 2D array is a simple change of syntax, which initialized a matrix row by row separated by ;
with spaces between individual elements. The function returns the same result because linear indexing works in 2d arrays in the column major order.
julia> typeof(a2d), eltype(a2d)
(Matrix{Int64}, Int64)
julia> polynomial(a2d, x)
128.0
The last example shows so called array comprehension syntax, where we define and array of known length using and for loop iteration. Resulting array/vector has integer elements, however even mixed type is possible yielding Any
, if there isn't any other common supertype to promote
every entry into. (Use ?
to look what promote
and promote_type
does.)
julia> typeof(ac), eltype(ac)
(Vector{Int64}, Int64)
julia> polynomial(ac, x)
108.0
So far we have seen that polynomial
function accepts a wide variety of arguments, however there are some understandable edge cases that it cannot handle.
Consider first the vector/array of characters ach
ach = ['1', '2', '3', '4']
4-element Vector{Char}:
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'3': ASCII/Unicode U+0033 (category Nd: Number, decimal digit)
'4': ASCII/Unicode U+0034 (category Nd: Number, decimal digit)
which themselves have numeric values (you can check by converting them to Int Int('1')
or convert(Int, '1')
). In spite of that, our untyped function cannot process such input, as there isn't an operation/method that would allow multiplication of Char
and Int
type. Julia tries to promote the argument types to some common type, however checking the promote_type(Int, Char)
returns Any
(union of all types), which tells us that the conversion is not possible automatically.
julia> typeof(ach), eltype(ach)
(Vector{Char}, Char)
julia> polynomial(ach, x)
ERROR: MethodError: no method matching *(::Float64, ::Char) The function `*` exists, but no method is defined for this combination of argument types. Closest candidates are: *(::Any, ::Any, ::Any, ::Any...) @ Base operators.jl:596 *(::Real, ::Complex{Bool}) @ Base complex.jl:330 *(::ChainRulesCore.NoTangent, ::Any) @ ChainRulesCore ~/.julia/packages/ChainRulesCore/6Pucz/src/tangent_arithmetic.jl:64 ...
In the stacktrace we can see the location of each function call. If we include the function polynomial
from some file poly.jl
using include("poly.jl")
, we will see that the location changes from REPL[X]:10
to the actual file name.
By swapping square brackets for round in the array comprehension ac
above, we have defined so called generator/iterator, which as opposed to original variable ac
does not allocate an array, only the structure that produces it.
ag = (2i^2 + 1 for i in -2:1)
typeof(ag), eltype(ag)
(Base.Generator{UnitRange{Int64}, Main.var"#3#4"}, Any)
You may notice that the element type in this case is Any
, which means that a function using this generator as an argument cannot specialize based on the type and has to infer it every time an element is generated/returned. We will touch on how this affects performance in one of the later lectures.
julia> polynomial(ag, x)
ERROR: MethodError: no method matching getindex(::Base.Generator{UnitRange{Int64}, Main.var"#3#4"}, ::Int64) The function `getindex` exists, but no method is defined for this combination of argument types.
The problem that we face during evaluation is that generator type is missing the getindex
operation, as they are made for situations where the size of the collection may be unknown and the only way of obtaining particular elements is through sequential iteration. Generators can be useful for example when creating batches of data for a machine learning training. We can "fix" the situation using collect
function, mentioned earlier, however that again allocates an array.
Extending/limiting the polynomial example
Following up on the polynomial example, let's us expand it a little further in order to facilitate the arguments, that have been throwing exceptions. The first direction, which we will move forward to, is providing the user with more detailed error message when an incorrect type of coefficients has been provided.
Design an if-else
condition such that the array of Char
example throws an error with custom string message, telling the user what went wrong and printing the incorrect input alongside it. Confirm that we have not broken the functionality of other examples from previous exercise.
HINTS:
- Throw the
ArgumentError(msg)
withthrow
function and string messagemsg
. More details in help mode?
or at the end of this document. - Strings are defined like this
s = "Hello!"
- Use string interpolation to create the error message. It allows injecting an expression into a string with the
$
syntaxb = 1; s = "Hellow Number $(b)"
- Compare
eltype
of the coefficients withChar
type. - The syntax for
if-else
:
if condition
println("true") # true branch code
else
println("false") # false branch code
end
- Not equal condition can be written as
a != b
. - Throwing an exception automatically returns from the function. Use return inside one of the branches to return the correct value.
Solution
The simplest way is to wrap the whole function inside an if-else
condition and returning only when the input is "correct" (it will still fail in some cases).
julia> function polynomial(a, x) if eltype(a) != Char accumulator = 0 for i in length(a):-1:1 accumulator += x^(i-1) * a[i] # ! 1-based indexing for arrays end return accumulator else throw(ArgumentError("Invalid coefficients $(a) of type Char!")) end end
polynomial (generic function with 1 method)
Now this should show our predefined error message.
julia> polynomial(ach, x)
ERROR: ArgumentError: Invalid coefficients ['1', '2', '3', '4'] of type Char!
Testing on other examples should pass without errors and give the same output as before.
julia> polynomial(a, x)
128.0
julia> polynomial(af, x)
128.0
julia> polynomial(at, x)
128.0
julia> polynomial(ant, x)
128.0
julia> polynomial(a2d, x)
128.0
julia> polynomial(ac, x)
108.0
The second direction concerns the limitation to index-able structures, which the generator example is not. For this we will have to rewrite the whole loop in a more functional programming approach using map
, anonymous function and other concepts.
Rewrite the following code inside our original polynomial
function with map
, enumerate
and anonymous function.
accumulator = 0
for i in length(a):-1:1
accumulator += x^(i-1) * a[i] # ! 1-based indexing for arrays
end
***Anonymous functions reminder***
julia> x -> x + 1 # unless the reference is stored it cannot be called
#1 (generic function with 1 method)
julia> plusone = x -> x + 1 # the reference can be stored inside a variable
#3 (generic function with 1 method)
julia> plusone(x) # calling with the same syntax
ERROR: UndefVarError: `x` not defined in `Main` Suggestion: check for spelling errors or missing imports.
HINTS:
- Use
enumerate
to obtain iterator overa
that returns a tuple ofia = (i, aᵢ)
. With Julia 1-based indexingi
starts also from 1 and goes up tolength(a)
. - Pass this into a
map
with either in-place or predefined anonymous function that does the operation ofx^(i-1) * aᵢ
. - Use
sum
to collect the resulting array intoaccumulator
variable or directly into thereturn
command.
BONUS: Can you figure out how to use the mapreduce
function here? See entry in the help mode ?
.
Solution
Ordered from the longest to the shortest, here are three examples with the same functionality (and there are definitely many more). Using the map(iterable) do itervar ... end
syntax, that creates anonymous function from the block of code.
function polynomial(a, x)
powers = map(enumerate(a)) do (i, aᵢ)
x^(i-1) * aᵢ
end
accumulator = sum(powers)
return accumulator
end
Using the default syntax for map
and storing the anonymous into a variable
function polynomial(a, x)
polypow(i,aᵢ) = x^(i-1) * aᵢ
powers = map(polypow, enumerate(a))
return sum(powers)
end
As the function polypow
is used only once, there is no need to assign it to a local variable. Note the sightly awkward additional parenthesis in the argument of the lambda function.
function polynomial(a, x)
powers = map(((i,aᵢ),) -> x^(i-1) * aᵢ, enumerate(a))
sum(powers)
end
Checking the behavior on all the inputs.
julia> polynomial(a, x)
128.0
julia> polynomial(af, x)
128.0
julia> polynomial(at, x)
128.0
julia> polynomial(ant, x)
128.0
julia> polynomial(a2d, x)
128.0
julia> polynomial(ach, x)
ERROR: MethodError: no method matching *(::Float64, ::Char) The function `*` exists, but no method is defined for this combination of argument types. Closest candidates are: *(::Any, ::Any, ::Any, ::Any...) @ Base operators.jl:596 *(::Real, ::Complex{Bool}) @ Base complex.jl:330 *(::ChainRulesCore.NoTangent, ::Any) @ ChainRulesCore ~/.julia/packages/ChainRulesCore/6Pucz/src/tangent_arithmetic.jl:64 ...
julia> polynomial(ac, x)
108.0
julia> polynomial(ag, x)
108.0
BONUS: You may have noticed that in the example above, the powers
variable is allocating an additional, unnecessary vector. With the current, scalar x
, this is not such a big deal. But in your homework you will generalize this function to matrix inputs of x
, which means that powers
becomes a vector of (potentially very large) matrices. This is a very natural use case for the mapreduce
: function:
polynomial(a, x) = mapreduce(+, enumerate(a), init=zero(x)) do (i, aᵢ)
x^(i-1) * aᵢ
end
polynomial(a, x)
128.0
Let's unpack what is happening here. If the function mapreduce(f, op, itr)
is called with op=+
it returns the same result as sum(map(f, itr))
. In contrast to sum(map(f, itr))
(which allocates a vector as a result of map
and then sums) mapreduce
applies f
to an element in itr
and immediately accumulates the result with the given op=+
.
julia> polynomial(a, x) = sum(ia -> x^(ia[1]-1) * ia[2], enumerate(a))
polynomial (generic function with 1 method)
How to use code from other people
The script that we have run at the beginning of this lab has created two new files inside the current folder:
./
├── Manifest.toml
└── Project.toml
Every folder with a toml file called Project.toml
, can be used by Julia's pkg manager into setting so called environment, which contains a list of pkgs to be installed. Setting up or more often called activating an environment can be done either before starting Julia itself by running julia with the --project XXX
flag or from within the Julia REPL, by switching to Pkg mode with ]
key (similar to the help mode activated by pressing ?
) and running command activate
.
So far we have used the general environment (depending on your setup), which by default does not come with any 3rd party packages and includes only the base and standard libraries - already quite powerful on its own.
In order to find which environment is currently active, run the following:
pkg> status
The output of such command usually indicates the general environment located at .julia/
folder (${HOME}/.julia/
or ${APPDATA}/.julia/
in case of Unix/Windows based systems respectively)
pkg> status
Status `~/.julia/environments/v1.6/Project.toml` (empty project)
Generally one should avoid working in the general environment, with the exception of some generic pkgs, such as PkgTemplates.jl
, which is used for generating library templates/folder structure like the one above (link), more on this in the lecture on pkg development.
Activate the environment inside the current folder and check that the BenchmarkTools
package has been installed. Use BenchmarkTools
pkg's @btime
to benchmark our polynomial
function with the following arguments.
aexp = ones(10) ./ factorial.(0:9)
x = 1.1
HINTS:
- In pkg mode use the command
activate
andstatus
to check the presence. - In order to import the functionality from other package, lookup the keyword
using
in the repl help mode?
. - The functionality that we want to use is the
@btime
macro (it acts almost like a function but with a different syntax@macro arg1 arg2 arg3 ...
). More on macros in lecture 7.
BONUS: Compare the output of polynomial(aexp, x)
with the value of exp(x)
, which it approximates.
In the assignment's code, we are using quite ubiquitous concept in Julia called broadcasting
or simply the dot-syntax
- represented here by ./
, factorial.
. This concept allows to map both simple arithmetic operations as well as custom functions across arrays, with the added benefit of increased performance, when the broadcasting system can merge operations into a more efficient code. More information can be found in the official documentation or section of our bachelor course.
Solution
There are other options to import a function/macro from a different package, however for now let's keep it simple with the using Module
syntax, that brings to the REPL, all the variables/function/macros exported by the BenchmarkTools
pkg. If @btime
is exported, which it is, it can be accessed without specification i.e. just by calling @btime
without the need for BenchmarkTools.@btime
. More on the architecture of pkg/module loading in the package developement lecture.
julia> using BenchmarkTools
julia> @btime polynomial(aexp, x)
97.119 ns (1 allocation: 16 bytes)
3.004165230550543
The output gives us the time of execution averaged over multiple runs (the number of samples is defined automatically based on run time) as well as the number of allocations and the output of the function, that is being benchmarked.
BONUS: The difference between our approximation and the "actual" function value computed as a difference of the two.
julia> polynomial(aexp, x) - exp(x)
-7.933958903194593e-7
The apostrophes in the previous sentence are on purpose, because implementation of exp
also relies on a finite sum, though much more sophisticated than the basic Taylor expansion.
Discussion & future directions
Instead of if-else
statements that would throw an error for different types, in Julia, we generally see the pattern of typing the function in a way, that for other than desirable types MethodError
is emitted with the information about closest matching methods. This is part of the design process in Julia of a function and for the particular functionality of the polynomial
example, we can look into the Julia itself, where it has been implemented in the evalpoly
function
julia> methods(evalpoly)
# 5 methods for generic function "evalpoly" from Base.Math: [1] evalpoly(z::Complex, p::AbstractVector) @ math.jl:147 [2] evalpoly(z::Complex, p::Tuple{Any}) @ math.jl:144 [3] evalpoly(z::Complex, p::Tuple) @ math.jl:119 [4] evalpoly(x, p::AbstractVector) @ math.jl:107 [5] evalpoly(x, p::Tuple) @ math.jl:94
Another avenue, that we have only touched with the BenchmarkTools
, is performance and will be further explored in the later lectures.
With the next lecture focused on typing in Julia, it is worth noting that polynomials lend themselves quite nicely to a definition of a custom type, which can help both readability of the code as well further extensions.
struct Polynom{C}
coefficients::{C}
end
function (p:Polynom)(x)
polynomial(p.coefficients, x)
end
Useful resources
- Getting Started tutorial from JuliaLang documentation - Docs
- Converting syntax between MATLAB ↔ Python ↔ Julia - Cheatsheet
- Bachelor course for refreshing your knowledge - Course
- Stylistic conventions - Style Guide
- Reserved keywords - List
- Official cheatsheet with basic syntax - link
Various errors and how to read them
This section summarizes most commonly encountered types of errors in Julia and how to resolve them or at least understand, what has gone wrong. It expands a little bit the official documentation, which contains the complete list with examples. Keep in mind again, that you can use help mode in the REPL to query error types as well.
MethodError
This type of error is most commonly thrown by Julia's multiple dispatch system with a message like no method matching X(args...)
, seen in two examples bellow.
julia> 2 * 'a' # many candidates
ERROR: MethodError: no method matching *(::Int64, ::Char) The function `*` exists, but no method is defined for this combination of argument types. Closest candidates are: *(::Any, ::Any, ::Any, ::Any...) @ Base operators.jl:596 *(::Real, ::Complex{Bool}) @ Base complex.jl:330 *(::ChainRulesCore.NoTangent, ::Any) @ ChainRulesCore ~/.julia/packages/ChainRulesCore/6Pucz/src/tangent_arithmetic.jl:64 ...
julia> getindex((i for i in 1:4), 3) # no candidates
ERROR: MethodError: no method matching getindex(::Base.Generator{UnitRange{Int64}, typeof(identity)}, ::Int64) The function `getindex` exists, but no method is defined for this combination of argument types.
Both of these examples have a short stacktrace, showing that the execution failed on the top most level in REPL
, however if this code is a part of some function in a separate file, the stacktrace will reflect it. What this error tells us is that the dispatch system could not find a method for a given function, that would be suitable for the type of arguments, that it has been given. In the first case Julia offers also a list of candidate methods, that match at least some of the arguments
When dealing with basic Julia functions and types, this behavior can be treated as something given and though one could locally add a method for example for multiplication of Char
and Int
, there is usually a good reason why Julia does not support such functionality by default. On the other hand when dealing with user defined code, this error may suggest the developer, that either the functions are too strictly typed or that another method definition is needed in order to satisfy the desired functionality.
InexactError
This type of error is most commonly thrown by the type conversion system (centered around convert
function), informing the user that it cannot exactly convert a value of some type to match arguments of a function being called.
julia> Int(1.2) # root cause
ERROR: InexactError: Int64(1.2)
julia> append!([1,2,3], 1.2) # same as above but shows the root cause deeper in the stack trace
ERROR: InexactError: Int64(1.2)
In this case the function being Int
and the value a floating point. The second example shows InexactError
may be caused deeper inside an inconspicuous function call, where we want to extend an array by another value, which is unfortunately incompatible.
ArgumentError
As opposed to the previous two errors, ArgumentError
can contain user specified error message and thus can serve multiple purposes. It is however recommended to throw this type of error, when the parameters to a function call do not match a valid signature, e.g. when factorial
were given negative or non-integer argument (note that this is being handled in Julia by multiple dispatch and specific DomainError
).
This example shows a concatenation of two 2d arrays of incompatible sizes 3x3 and 2x2.
julia> hcat(ones(3,3), zeros(2,2))
ERROR: DimensionMismatch: number of rows of each array must match (got (3, 2))
KeyError
This error is specific to hash table based objects such as the Dict
type and tells the user that and indexing operation into such structure tried to access or delete a non-existent element.
julia> d = Dict(:a => [1,2,3], :b => [1,23])
Dict{Symbol, Vector{Int64}} with 2 entries: :a => [1, 2, 3] :b => [1, 23]
julia> d[:c]
ERROR: KeyError: key :c not found
TypeError
Type assertion failure, or calling an intrinsic function (inside LLVM, where code is strictly typed) with incorrect argument type. In practice this error comes up most often when comparing value of a type against the Bool
type as seen in the example bellow.
julia> if 1 end # calls internally typeassert(1, Bool)
ERROR: TypeError: non-boolean (Int64) used in boolean context
julia> typeassert(1, Bool)
ERROR: TypeError: non-boolean (Int64) used in boolean context
In order to compare inside conditional statements such as if-elseif-else
or the ternary operator x ? a : b
the condition has to be always of Bool
type, thus the example above can be fixed by the comparison operator: if 1 == 1 end
(in reality either the left or the right side of the expression contains an expression or a variable to compare against).
UndefVarError
While this error is quite self-explanatory, the exact causes are often quite puzzling for the user. The reason behind the confusion is to do with code scoping, which comes into play for example when trying to access a local variable from outside of a given function or just updating a global variable from within a simple loop.
In the first example we show the former case, where variable is declared from within a function and accessed from outside afterwards.
julia> function plusone(x) uno = 1 return x + uno end
plusone (generic function with 1 method)
julia> uno # defined only within plusone
ERROR: UndefVarError: `uno` not defined in `Main` Suggestion: check for spelling errors or missing imports.
Unless there is variable I_am_not_defined
in the global scope, the following should throw an error.
julia> I_am_not_defined
ERROR: UndefVarError: `I_am_not_defined` not defined in `Main` Suggestion: check for spelling errors or missing imports.
Often these kind of errors arise as a result of bad code practices, such as long running sessions of Julia having long forgotten global variables, that do not exist upon new execution (this one in particular has been addressed by the authors of the reactive Julia notebooks Pluto.jl).
For more details on code scoping we recommend particular places in the bachelor course lectures here and there.
ErrorException
& error
function
ErrorException
is the most generic error, which can be thrown/raised just by calling the error
function with a chosen string message. As a result developers may be inclined to misuse this for any kind of unexpected behavior a user can run into, often providing out-of-context/uninformative messages.