Getting started with Julia
This tutorial was generated using Literate.jl. Download the source as a .jl
file.
Because JuMP is embedded in Julia, knowing some basic Julia is important before you start learning JuMP.
This tutorial is designed to provide a minimalist crash course in the basics of Julia. You can find resources that provide a more comprehensive introduction to Julia here.
Installing Julia
To install Julia, download the latest stable release, then follow the platform specific install instructions.
Unless you know otherwise, you probably want the 64-bit version.
Next, you need an IDE to develop in. VS Code is a popular choice, so follow these install instructions.
Julia can also be used with Jupyter notebooks or the reactive notebooks of Pluto.jl.
The Julia REPL
The main way of interacting with Julia is via its REPL (Read Evaluate Print Loop). To access the REPL, start the Julia executable to arrive at the julia>
prompt, and then start coding:
julia> 1 + 1
2
As your programs become larger, write a script as a text file, and then run that file using:
julia> include("path/to/file.jl")
Because of Julia's startup latency, running scripts from the command line like the following is slow:
$ julia path/to/file.jl
Use the REPL or a notebook instead, and read The "time-to-first-solve" issue for more information.
Code blocks in this documentation
In this documentation you'll see a mix of code examples with and without the julia>
.
The Julia prompt is mostly used to demonstrate short code snippets, and the output is exactly what you will see if run from the REPL.
Blocks without the julia>
can be copy-pasted into the REPL, but they are used because they enable richer output like plots or LaTeX to be displayed in the online and PDF versions of the documentation. If you run them from the REPL you may see different output.
Where to get help
- Read the documentation
- Ask (or browse) the Julia community forum: https://discourse.julialang.org
- If the question is JuMP-related, ask in the Optimization (Mathematical) section, or tag your question with "jump"
To access the built-in help at the REPL, type ?
to enter help-mode, followed by the name of the function to lookup:
help?> print
search: print println printstyled sprint isprint prevind parentindices precision escape_string
print([io::IO], xs...)
Write to io (or to the default output stream stdout if io is not given) a canonical
(un-decorated) text representation. The representation used by print includes minimal formatting
and tries to avoid Julia-specific details.
print falls back to calling show, so most types should just define show. Define print if your
type has a separate "plain" representation. For example, show displays strings with quotes, and
print displays strings without quotes.
string returns the output of print as a string.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> print("Hello World!")
Hello World!
julia> io = IOBuffer();
julia> print(io, "Hello", ' ', :World!)
julia> String(take!(io))
"Hello World!"
Numbers and arithmetic
Since we want to solve optimization problems, we're going to be using a lot of math. Luckily, Julia is great for math, with all the usual operators:
julia> 1 + 1
2
julia> 1 - 2
-1
julia> 2 * 2
4
julia> 4 / 5
0.8
julia> 3^2
9
Did you notice how Julia didn't print .0
after some of the numbers? Julia is a dynamic language, which means you never have to explicitly declare the type of a variable. However, in the background, Julia is giving each variable a type. Check the type of something using the typeof
function:
julia> typeof(1)
Int64
julia> typeof(1.0)
Float64
Here 1
is an Int64
, which is an integer with 64 bits of precision, and 1.0
is a Float64
, which is a floating point number with 64-bits of precision.
If you aren't familiar with floating point numbers, make sure to read the Floating point numbers section.
We create complex numbers using im
:
julia> x = 2 + 1im
2 + 1im
julia> real(x)
2
julia> imag(x)
1
julia> typeof(x)
Complex{Int64}
julia> x * (1 - 2im)
4 - 3im
The curly brackets surround what we call the parameters of a type. You can read Complex{Int64}
as "a complex number, where the real and imaginary parts are represented by Int64
." If we call typeof(1.0 + 2.0im)
it will be Complex{Float64}
, which a complex number with the parts represented by Float64
.
There are also some cool things like an irrational representation of π.
julia> π
π = 3.1415926535897...
To make π (and most other Greek letters), type \pi
and then press [TAB]
.
julia> typeof(π)
Irrational{:π}
However, if we do math with irrational numbers, they get converted to Float64
:
julia> typeof(2π / 3)
Float64
Floating point numbers
If you aren't familiar with floating point numbers, make sure to read this section carefully.
A Float64
is a floating point approximation of a real number using 64-bits of information.
Because it is an approximation, things we know hold true in mathematics don't hold true in a computer. For example:
julia> 0.1 * 3 == 0.3
false
A more complicated example is:
julia> sin(2π / 3) == √3 / 2
false
Get √
by typing \sqrt
then press [TAB]
.
Let's see what the differences are:
julia> 0.1 * 3 - 0.3
5.551115123125783e-17
julia> sin(2π / 3) - √3 / 2
1.1102230246251565e-16
They are small, but not zero.
One way of explaining this difference is to consider how we would write 1 / 3
and 2 / 3
using only four digits after the decimal point. We would write 1 / 3
as 0.3333
, and 2 / 3
as 0.6667
. So, despite the fact that 2 * (1 / 3) == 2 / 3
, 2 * 0.3333 == 0.6666 != 0.6667
.
Let's try that again using ≈ (\approx + [TAB]
) instead of ==
:
julia> 0.1 * 3 ≈ 0.3
true
julia> sin(2π / 3) ≈ √3 / 2
true
≈
is a clever way of calling the isapprox
function:
julia> isapprox(sin(2π / 3), √3 / 2; atol = 1e-8)
true
Floating point is the reason solvers use tolerances when they solve optimization models. A common mistake you're likely to make is checking whether a binary variable is 0 using value(z) == 0
. Always remember to use something like isapprox
when comparing floating point numbers.
Note that isapprox
will always return false
if one of the number being compared is 0
and atol
is zero (its default value).
julia> 1e-300 ≈ 0.0
false
so always set a nonzero value of atol
if one of the arguments can be zero.
julia> isapprox(1e-9, 0.0; atol = 1e-8)
true
Gurobi has a good series of articles on the implications of floating point in optimization if you want to read more.
If you aren't careful, floating point arithmetic can throw up all manner of issues. For example:
julia> 1 + 1e-16 == 1
true
It even turns out that floating point numbers aren't associative:
julia> (1 + 1e-16) - 1e-16 == 1 + (1e-16 - 1e-16)
false
It's important to note that this issue isn't Julia-specific. It happens in every programming language (try it out in Python).
Vectors, matrices, and arrays
Similar to MATLAB, Julia has native support for vectors, matrices and tensors; all of which are represented by arrays of different dimensions. Vectors are constructed by comma-separated elements surrounded by square brackets:
julia> b = [5, 6]
2-element Vector{Int64}: 5 6
Matrices can be constructed with spaces separating the columns, and semicolons separating the rows:
julia> A = [1.0 2.0; 3.0 4.0]
2×2 Matrix{Float64}: 1.0 2.0 3.0 4.0
We can do linear algebra:
julia> x = A \ b
2-element Vector{Float64}: -3.9999999999999987 4.499999999999999
Here is floating point at work again; x
is approximately [-4, 4.5]
.
julia> A * x
2-element Vector{Float64}: 5.0 6.0
julia> A * x ≈ b
true
Note that when multiplying vectors and matrices, dimensions matter. For example, you can't multiply a vector by a vector:
julia> b * b
MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64}) 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 *(::MutableArithmetics.Zero, ::Any) @ MutableArithmetics ~/.julia/packages/MutableArithmetics/BLlgj/src/rewrite.jl:61 *(::Any, ::MutableArithmetics.Zero) @ MutableArithmetics ~/.julia/packages/MutableArithmetics/BLlgj/src/rewrite.jl:62 ...
But multiplying transposes works:
julia> b' * b
61
julia> b * b'
2×2 Matrix{Int64}: 25 30 30 36
Other common types
Comments
Although not technically a type, code comments begin with the #
character:
julia> 1 + 1 # This is a comment
2
Multiline comments begin with #=
and end with =#
:
#=
Here is a
multiline comment
=#
Comments can even be nested inside expressions. This is sometimes helpful when documenting inputs to functions:
julia> isapprox( sin(π), 0.0; #= We need an explicit atol here because we are comparing with 0 =# atol = 0.001, )
true
Strings
Double quotes are used for strings:
julia> typeof("This is Julia")
String
Unicode is fine in strings:
julia> typeof("π is about 3.1415")
String
Use println
to print a string:
julia> println("Hello, World!")
Hello, World!
Use $()
to interpolate values into a string:
julia> x = 123
123
julia> println("The value of x is: $(x)")
The value of x is: 123
Use triple-quotes for multiline strings:
julia> s = """ Here is a multiline string """
"Here is\na\nmultiline string\n"
julia> println(s)
Here is a multiline string
Symbols
Julia Symbol
s are a data structure from the compiler that represent Julia identifiers (that is, variable names).
julia> println("The value of x is: $(eval(:x))")
The value of x is: 123
We used eval
here to demonstrate how Julia links Symbol
s to variables. However, avoid calling eval
in your code. It is usually a sign that your code is doing something that could be more easily achieved a different way. The Community Forum is a good place to ask for advice on alternative approaches.
julia> typeof(:x)
Symbol
You can think of a Symbol
as a String
that takes up less memory, and that can't be modified.
Convert between String
and Symbol
using their constructors:
julia> String(:abc)
"abc"
julia> Symbol("abc")
:abc
Symbol
s are often (ab)used to stand in for a String
or an Enum
, when one of the latter is likely a better choice. The JuMP Style guide recommends reserving Symbol
s for identifiers. See @enum vs. Symbol for more.
Tuples
Julia makes extensive use of a simple data structure called Tuples. Tuples are immutable collections of values. For example:
julia> t = ("hello", 1.2, :foo)
("hello", 1.2, :foo)
julia> typeof(t)
Tuple{String, Float64, Symbol}
Tuples can be accessed by index, similar to arrays:
julia> t[2]
1.2
And they can be "unpacked" like so:
julia> a, b, c = t
("hello", 1.2, :foo)
julia> b
1.2
The values can also be given names, which is a convenient way of making light-weight data structures.
julia> t = (word = "hello", num = 1.2, sym = :foo)
(word = "hello", num = 1.2, sym = :foo)
Values can be accessed using dot syntax:
julia> t.word
"hello"
Dictionaries
Similar to Python, Julia has native support for dictionaries. Dictionaries provide a very generic way of mapping keys to values. For example, a map of integers to strings:
julia> d1 = Dict(1 => "A", 2 => "B", 4 => "D")
Dict{Int64, String} with 3 entries: 4 => "D" 2 => "B" 1 => "A"
Type-stuff again: Dict{Int64,String}
is a dictionary with Int64
keys and String
values.
Looking up a value uses the bracket syntax:
julia> d1[2]
"B"
Dictionaries support non-integer keys and can mix data types:
julia> Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im)
Dict{String, Number} with 3 entries: "B" => 2.5 "A" => 1 "D" => 2-3im
Julia types form a hierarchy. Here the value type of the dictionary is Number
, which is a generalization of Int64
, Float64
, and Complex{Int}
. Leaf nodes in this hierarchy are called "concrete" types, and all others are called "Abstract." In general, having variables with abstract types like Number
can lead to slower code, so you should try to make sure every element in a dictionary or vector is the same type. For example, in this case we could represent every element as a Complex{Float64}
:
julia> Dict("A" => 1.0 + 0.0im, "B" => 2.5 + 0.0im, "D" => 2.0 - 3.0im)
Dict{String, ComplexF64} with 3 entries: "B" => 2.5+0.0im "A" => 1.0+0.0im "D" => 2.0-3.0im
Dictionaries can be nested:
julia> d2 = Dict("A" => 1, "B" => 2, "D" => Dict(:foo => 3, :bar => 4))
Dict{String, Any} with 3 entries: "B" => 2 "A" => 1 "D" => Dict(:bar=>4, :foo=>3)
julia> d2["B"]
2
julia> d2["D"][:foo]
3
Structs
You can define custom datastructures with struct
:
julia> struct MyStruct x::Int y::String z::Dict{Int,Int} end
julia> a = MyStruct(1, "a", Dict(2 => 3))
Main.MyStruct(1, "a", Dict(2 => 3))
julia> a.x
1
By default, these are not mutable
julia> a.x = 2
setfield!: immutable struct of type MyStruct cannot be changed
However, you can declare a mutable struct
which is mutable:
julia> mutable struct MyStructMutable x::Int y::String z::Dict{Int,Int} end
julia> a = MyStructMutable(1, "a", Dict(2 => 3))
Main.MyStructMutable(1, "a", Dict(2 => 3))
julia> a.x
1
julia> a.x = 2
2
julia> a
Main.MyStructMutable(2, "a", Dict(2 => 3))
Loops
Julia has native support for for-each style loops with the syntax for <value> in <collection> end
:
julia> for i in 1:5 println(i) end
1 2 3 4 5
Ranges are constructed as start:stop
, or start:step:stop
.
julia> for i in 1.2:1.1:5.6 println(i) end
1.2 2.3 3.4 4.5 5.6
This for-each loop also works with dictionaries:
julia> for (key, value) in Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im) println("$(key): $(value)") end
B: 2.5 A: 1 D: 2 - 3im
Note that in contrast to vector languages like MATLAB and R, loops do not result in a significant performance degradation in Julia.
Control flow
Julia control flow is similar to MATLAB, using the keywords if-elseif-else-end
, and the logical operators ||
and &&
for or and and respectively:
julia> for i in 0:5:15 if i < 5 println("$(i) is less than 5") elseif i < 10 println("$(i) is less than 10") else if i == 10 println("the value is 10") else println("$(i) is bigger than 10") end end end
0 is less than 5 5 is less than 10 the value is 10 15 is bigger than 10
Comprehensions
Similar to languages like Haskell and Python, Julia supports the use of simple loops in the construction of arrays and dictionaries, called comprehensions.
A list of increasing integers:
julia> [i for i in 1:5]
5-element Vector{Int64}: 1 2 3 4 5
Matrices can be built by including multiple indices:
julia> [i * j for i in 1:5, j in 5:10]
5×6 Matrix{Int64}: 5 6 7 8 9 10 10 12 14 16 18 20 15 18 21 24 27 30 20 24 28 32 36 40 25 30 35 40 45 50
Conditional statements can be used to filter out some values:
julia> [i for i in 1:10 if i % 2 == 1]
5-element Vector{Int64}: 1 3 5 7 9
A similar syntax can be used for building dictionaries:
julia> Dict("$(i)" => i for i in 1:10 if i % 2 == 1)
Dict{String, Int64} with 5 entries: "1" => 1 "5" => 5 "7" => 7 "9" => 9 "3" => 3
Functions
A simple function is defined as follows:
julia> function print_hello() return println("hello") end
print_hello (generic function with 1 method)
julia> print_hello()
hello
Arguments can be added to a function:
julia> function print_it(x) return println(x) end
print_it (generic function with 1 method)
julia> print_it("hello")
hello
julia> print_it(1.234)
1.234
julia> print_it(:my_id)
my_id
Optional keyword arguments are also possible:
julia> function print_it(x; prefix = "value:") return println("$(prefix) $(x)") end
print_it (generic function with 1 method)
julia> print_it(1.234)
value: 1.234
julia> print_it(1.234; prefix = "val:")
val: 1.234
The keyword return
is used to specify the return values of a function:
julia> function mult(x; y = 2.0) return x * y end
mult (generic function with 1 method)
julia> mult(4.0)
8.0
julia> mult(4.0; y = 5.0)
20.0
Anonymous functions
The syntax input -> output
creates an anonymous function. These are most useful when passed to other functions. For example:
julia> f = x -> x^2
#11 (generic function with 1 method)
julia> f(2)
4
julia> map(x -> x^2, 1:4)
4-element Vector{Int64}: 1 4 9 16
Type parameters
We can constrain the inputs to a function using type parameters, which are ::
followed by the type of the input we want. For example:
julia> function foo(x::Int) return x^2 end
foo (generic function with 1 method)
julia> function foo(x::Float64) return exp(x) end
foo (generic function with 2 methods)
julia> function foo(x::Number) return x + 1 end
foo (generic function with 3 methods)
julia> foo(2)
4
julia> foo(2.0)
7.38905609893065
julia> foo(1 + 1im)
2 + 1im
But what happens if we call foo
with something we haven't defined it for?
julia> foo([1, 2, 3])
MethodError: no method matching foo(::Vector{Int64}) The function `foo` exists, but no method is defined for this combination of argument types. Closest candidates are: foo(::Float64) @ Main REPL[2]:1 foo(::Int64) @ Main REPL[1]:1 foo(::Number) @ Main REPL[3]:1
A MethodError
means that you passed a function something that didn't match the type that it was expecting. In this case, the error message says that it doesn't know how to handle an Vector{Int64}
, but it does know how to handle Float64
, Int64
, and Number
.
Read the "Closest candidates" part of the error message carefully to get a hint as to what was expected.
Broadcasting
In the example above, we didn't define what to do if f
was passed a Vector
. Luckily, Julia provides a convenient syntax for mapping f
element-wise over arrays. Just add a .
between the name of the function and the opening (
. This works for any function, including functions with multiple arguments. For example:
julia> foo.([1, 2, 3])
3-element Vector{Int64}: 1 4 9
Get a MethodError
when calling a function that takes a Vector
, Matrix
, or Array
? Try broadcasting.
Mutable vs immutable objects
Some types in Julia are mutable, which means you can change the values inside them. A good example is an array. You can modify the contents of an array without having to make a new array.
In contrast, types like Float64
are immutable. You cannot modify the contents of a Float64
.
This is something to be aware of when passing types into functions. For example:
julia> function mutability_example(mutable_type::Vector{Int}, immutable_type::Int) mutable_type[1] += 1 immutable_type += 1 return end
mutability_example (generic function with 1 method)
julia> mutable_type = [1, 2, 3]
3-element Vector{Int64}: 1 2 3
julia> immutable_type = 1
1
julia> mutability_example(mutable_type, immutable_type)
julia> println("mutable_type: $(mutable_type)")
mutable_type: [2, 2, 3]
julia> println("immutable_type: $(immutable_type)")
immutable_type: 1
Because Vector{Int}
is a mutable type, modifying the variable inside the function changed the value outside of the function. In contrast, the change to immutable_type
didn't modify the value outside the function.
You can check mutability with the isimmutable
function:
julia> isimmutable([1, 2, 3])
false
julia> isimmutable(1)
true
The package manager
Installing packages
No matter how wonderful Julia's base language is, at some point you will want to use an extension package. Some of these are built-in, for example random number generation is available in the Random
package in the standard library. These packages are loaded with the commands using
and import
.
julia> using Random # The equivalent of Python's `from Random import *`
julia> import Random # The equivalent of Python's `import Random`
julia> Random.seed!(33)
Random.TaskLocalRNG()
julia> [rand() for i in 1:10]
10-element Vector{Float64}: 0.4745319377345316 0.9650392357070774 0.8194019096093067 0.9297749959069098 0.3127122336048005 0.9684448191382753 0.9063743823581542 0.8386731983150535 0.5103924401614957 0.9296414851080324
The Package Manager is used to install packages that are not part of Julia's standard library.
For example the following can be used to install JuMP,
using Pkg
Pkg.add("JuMP")
For a complete list of registered Julia packages see the package listing at JuliaHub.
From time to you may wish to use a Julia package that is not registered. In this case a git repository URL can be used to install the package.
using Pkg
Pkg.add("https://github.com/user-name/MyPackage.jl.git")
Package environments
By default, Pkg.add
will add packages to Julia's global environment. However, Julia also has built-in support for virtual environments.
Activate a virtual environment with:
import Pkg; Pkg.activate("/path/to/environment")
You can see what packages are installed in the current environment with Pkg.status()
.
We strongly recommend you create a Pkg environment for each project that you create in Julia, and add only the packages that you need, instead of adding lots of packages to the global environment. The Pkg manager documentation has more information on this topic.