TLDR: The Julia REPL has 4 modes: Julia, package (]
), help (?
) and shell (;
).
The Read-Eval-Print Loop (or REPL) is the most basic way to interact with Julia, check out its documentation for details.
You can start a REPL by typing julia
into a terminal, or by clicking on the Julia application in your computer.
It will allow you to play around with arbitrary Julia code:
julia> a, b = 1, 2;
julia> a + b
3
This is the standard (Julia) mode of the REPL, but there are three other modes you need to know.
Each mode is entered by typing a specific character after the julia>
prompt.
Once you're in a non-Julia mode, you stay there for every command you run.
To exit it, hit backspace after the prompt and you'll get the julia>
prompt back.
?
)By pressing ?
you can obtain information and metadata about Julia objects (functions, types, etc.) or unicode symbols.
The query fetches the docstring of the object, which explains how to use it.
help?> println
println([io::IO], xs...)
Print (using ) xs
to io
followed by a newline. If io
is not supplied, prints to the default output stream .
See also to add colors etc.
julia> println("Hello, world")
Hello, world
julia> io = IOBuffer();
julia> println(io, "Hello", ',', " world.")
julia> String(take!(io))
"Hello, world.\n"
If you don't know the exact name you are looking for, type a word surrounded by quotes to see in which docstrings it pops up.
]
)By pressing ]
you access Pkg.jl, Julia's integrated package manager, whose documentation is an absolute must-read.
Pkg.jl allows you to:
]activate
different local, shared or temporary environments;]instantiate
them by downloading the necessary packages;]add
, ]update
(or ]up
) and ]remove
(or ]rm
) packages;]status
(or ]st
) of your current environment.As an illustration, we download the package Example.jl inside our current environment:
(JuliaIntro) pkg> add Example
โ Warning: The Pkg REPL mode is intended for interactive use only, and should not be used from scripts. It is recommended to use the functional API instead.
โ @ Pkg.REPLMode /opt/hostedtoolcache/julia/1.11.6/x64/share/julia/stdlib/v1.11/Pkg/src/REPLMode/REPLMode.jl:388
Resolving package versions...
Installed Example โ v0.5.5
Updating `~/work/JuliaIntro/JuliaIntro/Project.toml`
[7876af07] + Example v0.5.5
Updating `~/work/JuliaIntro/JuliaIntro/Manifest.toml`
[7876af07] + Example v0.5.5
Precompiling project...
399.8 ms โ Example
1 dependency successfully precompiled in 1 seconds. 303 already precompiled.
(JuliaIntro) pkg> status
Status `~/work/JuliaIntro/JuliaIntro/Project.toml`
[1520ce14] AbstractTrees v0.4.5
[6e4b80f9] BenchmarkTools v1.6.0
[13f3f980] CairoMakie v0.15.3
[7876af07] Example v0.5.5
[cd3eb016] HTTP v1.10.17
[98b081ad] Literate v2.20.1
[2bd173c7] NodeJS v2.0.0
[c3e4b0f8] Pluto v0.20.13
[b77e0a4c] InteractiveUtils v1.11.0
[37e2e46d] LinearAlgebra v1.11.0
Note that the same keywords are also available in Julia mode:
julia> using Pkg
julia> Pkg.rm("Example")
Updating `~/work/JuliaIntro/JuliaIntro/Project.toml`
[7876af07] - Example v0.5.5
Updating `~/work/JuliaIntro/JuliaIntro/Manifest.toml`
[7876af07] - Example v0.5.5
Info We haven't cleaned this depot up for a bit, running Pkg.gc()...
Active manifest files: 2 found
Active artifact files: 60 found
Active scratchspaces: 1 found
Deleted no artifacts, repos, packages or scratchspaces
The package mode itself also has a help mode, accessed with ?
, in case you're lost among all these new keywords.
;
)By pressing ;
you enter a terminal, where you can execute any command you want.
Here's an example for Unix systems:
TLDR: Activate a local environment for each project with ]activate path
. Its details are stored in Project.toml
and Manifest.toml
.
As we have seen, Pkg.jl is the Julia equivalent of pip
or conda
for Python.
It lets you install packages and manage environments (collections of packages with specific versions).
You can activate an environment from the Pkg REPL by specifying its path ]activate somepath
.
Typically, you would do ]activate .
to activate the environment in the current directory.
Another option is to directly start Julia inside an environment, with the command line option julia --project=somepath
.
Once in an environment, the packages you ]add
will be listed in two files somepath/Project.toml
and somepath/Manifest.toml
:
Project.toml
contains general project information (name of the package, unique id, authors) and direct dependencies with version bounds.Manifest.toml
contains the exact versions of all direct and indirect dependenciesIf you haven't entered any local project, packages will be installed in the default environment, called @v1.X
after the active version of Julia (note the @
before the name).
Packages installed that way are available no matter which local environment is active, because of "environment stacking".
It is recommended to keep the default environment very light to avoid dependencies conflicts. It should contain only essential development tools.
VSCode: You can configure the environment in which a VSCode Julia REPL opens.
Just click the Julia env: ...
button at the bottom.
Note however that the Julia version itself will always be the default one from juliaup
.
Advanced: You can visualize the dependency graph of an environment with PkgDependency.jl.
We're going to explore how package management and code structure typically works using this repository
Julia has a fairly simple syntax which is:
Important - Everything in Julia is an expression:
(Almost) every piece of syntax in Julia is an expression. That means that is has an associated value. For instance the expression 2 + 2
has the value 4
.
Keep this in mind as we go through the code.
Before we get into basic syntax we will need a few packages for this tutorial. While you can load packages at any top-level scope, it's best practice to do so at the beginning of your file, package, or notebook.
using LinearAlgebra, Random, CairoMakie # for plotting
A literal is a representation of some data directly in code. For instance:
4
4
is the literal for the 64-bit Integer number \(4\).
Note: Since this is an expression is has a value of 4.
Notebooks and the REPL will automatically print out the value of an expression (or the last expression in a code block), unless the value of an expression is the special constant:
nothing
The other way to suppress the output of an expression is to use ;
character which can also be used to delimit multiple statements on a single line.
5; 6; 7;
Other literals include:
"Howdy folks!"; # string
3.14159265 # floating point (Float64)
true # Boolean
[1, 2, 3, 4] # Vector of Int64
4-element Vector{Int64}:
1
2
3
4
(1, 3.14159, "Quantum? Never met 'em!") # A Tuple (an immutable fixed length collection)
(1, 3.14159, "Quantum? Never met 'em!")
A symbol is an identifier, a set of characters prefixed by a colon. Mostly used internally for Julia, they are essentially how you represent names (variables) as data. They are a little different than strings, but you won't see them often.
:iamasymbol
:๐ฑ
:๐ฑ
Now that we have literals we might like to give them a name. We can assign literals (and any other data) to a variable, which is simply a named value:
a = [1,2,3,4]
w = 10
y = true
true
We can interpolate into strings by either $y
or $(w + 2)
for more complex expressions
x = "Are are at UMass Amherst? $y"
"Are are at UMass Amherst? true"
Assignments are an expression whose value is the right-hand-side. This lets us, among other things, chain assignments:
i = j = 5; # both i and j are equal to 5
@show i j
i = 5
j = 5
Variables are not constrained to a particular type.
x = 13.5 # reassign x to a different type.
13.5
Julia has support for Unicode variable names.
In VSCode and the REPL we can use autocompletion such as \Sigma
-TAB-\hat
-TAB-\^2
-TAB
ฮฃฬยฒ = 1.0;
There is complete support for all of Unicode, so that means we have emoji as well!
๐ณ = 5 # `\:flushed`-TAB
โ๏ธ = "Quantum Numerics!" # I don't think this has an autocomplete yet ๐ค
"Quantum Numerics!"
Julia allows for numeric literals to be immediately followed by a variable, which results in multiplication:
3x
40.5
This helps us write long equations very nicely:
4(x + 3)^2 - 10
1079.0
This functionality is also used for complex number literals, with the global constant im
:
10 + 3im
10 + 3im
Like most languages Julia has two syntaxes for operating on values: operators and function calls.
w + x + ฮฃฬยฒ
24.5
2 ^ 5 # exponentiation
32
There are also Unicode operators, for instance this is integer division:
5 รท 2 # (`5 \div`-TAB-`2`)
2
โ8 # `\cbrt`-TAB-`8`
2.0
The boolean operators are:
!y # negation
false && y # short circuiting and
y || true # short circuiting or
true
The <op>=
operators update a variable, such as:
w += 3
13
Functions are called much as they are in other languages like Python:
sin(2x)
0.956375928404503
length(a)
4
complex(1.0, 10)
1.0 + 10.0im
There are three syntaxes for defining functions. The first two produce named functions, and we'll get to the third in a moment. The general form is:
function foo(x, y)
# we don't actually need the `return` here
# the last expression is returned by default
return x + y
end
foo (generic function with 1 method)
and the terse form which is typically used if your function is a single expression:
bar(x, y) = x + y
bar (generic function with 1 method)
We can call this function as above:
@show foo(w, x) == bar(w, x)
foo(w, x) == bar(w, x) = true
We can add "keyword" arguments to a function by placing them after a semicolon in the arguments:
function mykwargs(a; b)
a ^ b
end;
Both positional arguments and keyword arguments can be made optional by providing a default value.
Ignore the @show
for now we'll get to that later, it prints out the expression and its result.
function myoptionalfunc(a = 2; b = 3)
a ^ b
end
@show myoptionalfunc()
@show myoptionalfunc(b = 3)
@show myoptionalfunc(5)
@show myoptionalfunc(5, b = 2)
myoptionalfunc() = 8
myoptionalfunc(b = 3) = 8
myoptionalfunc(5) = 125
myoptionalfunc(5, b = 2) = 25
Operators are "just" functions with support for infix notation (except &&
and ||
which uniquely short circuit)
+(1, 2, 3, 4)
10
f = *
f(1, 2, 3)
6
Just like strings or vectors and can be assigned to variables:
baz = foo
baz(w, x)
26.5
They can also be passed to other functions:
map(sin, [1.0, 2.0, ฯ])
3-element Vector{Float64}:
0.8414709848078965
0.9092974268256817
1.2246467991473532e-16
The third syntax for defining a function is for so-called anonymous functions (functions without a name)
x -> 5x + (x - 1)^2
#3 (generic function with 1 method)
Of course we didn't give it a name, so now we have no way to reference it! These are most often used to pass to "higher-order" functions (functions that accept other functions as arguments):
map(x -> 5x + (x - 1)^2, a)
4-element Vector{Int64}:
5
11
19
29
For anonymous functions with multiple arguments the syntax looks like this:
(x, y, z) -> x/3 + y^3 + z^2
#7 (generic function with 1 method)
or with zero arguments like this:
() -> 2 + 2
#9 (generic function with 1 method)
We often encounter situations where we want to call a function such as sin
over all elements of a collection like a Vector
.
In order to make this ergonomic Julia supports a generalized "broadcasting" syntax:
sin.([1, 2, 3])
3-element Vector{Float64}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
Any function can be suffixed with a .
in order to broadcast it elementwise over each value in a collection.
This also works elementwise, for instance:
complex.([1, 2, 3], [10, 20, 30])
3-element Vector{Complex{Int64}}:
1 + 10im
2 + 20im
3 + 30im
or with operators in infix form:
[1, 2, 3] .+ [4, 5, 6]
3-element Vector{Int64}:
5
7
9
We now know how to define variables, call functions and operators, and define basic functions. For a bit of a break let's look at the following function which computes a single point in the mandelbrot set to get a sense for control flow:
function mandel(z)
c = z
maxiter = 80
for n in 1:maxiter
if abs(z) > 2
return n-1
end
z = z^2 + c
end
return maxiter
end;
We need a complex plane to compute the set on:
resolution = 0.02
reals = -2:resolution:1 # range from -2 to 1 with step size of 0.02
imgs = -1:resolution:1
plane = [complex(re, img) for (re, img) in Iterators.product(reals, imgs)];
results = mandel.(plane);
We're going to use a package called CairoMakie to plot the result:
CairoMakie.heatmap(results)
Let's break down some of the expressions we saw above:
We have a conditional block above which tests whether abs(z) > 2
.
A general control flow block looks something like:
x = 5; y = 10; z = true
if x < y
println("Small x!")
elseif x == y
println("x and y are equal!")
else
println("Big x!")
end
Small x!
Many languages have a ternary expression, for a short way to evaluate a two way conditional:
println(x < y ? "x is less than y!" : "x is greater than or equal to y!")
x is less than y!
Julia users really like to keep code compact, so there is one additional conditional expression you might see:
x < y && println("x was less than y!")
x < y || println("I don't get here if x < y!")
x was less than y!
true
It's important to note that conditional expressions (like if-elseif-else
) are short-circuiting.
This means that in the expression:
a && b
b
is only evaluated if a
is true
!
Similarly in
a || b
b
will only be evaluated if a = false
Our mandelbrot
function also has a for
loop iterating over the range 1:maxiter
.
There are two loop constructs in Julia. while
loops:
i = 1;
while i <= 5
println("i = $i")
i += 1
end
i = 1
i = 2
i = 3
i = 4
i = 5
and for
loops (note that i
is local to the for
loop block, unlike with the while
above):
for i in 1:2:9
println("i = $i")
end
i = 1
i = 3
i = 5
i = 7
i = 9
for
loops can iterate over many different containers, including the Vector
s, Tuple
s,
StepRange
s like 1:2:9
, UnitRange
s like 1:10
and more.
Instead of the in
keyword we can also use =
and โ
(\in
-TAB).
When we are looping over containers like Vector
s it's important to be sure we know what we are looping over!
We will get to indexing when we talk about arrays in the next section, but for now we can index a Vector
as follows:
emojis = ["๐ค", "โ๏ธ", "๐ป"];
@show emojis[1]
@show emojis[3]
emojis[1] = "๐ค"
emojis[3] = "๐ป"
Notice that Julia is (generally) 1-based, not 0 based!
println("Iterating over each value:")
for i โ emojis
println(i)
end
println("Iterating over the indices:")
for i โ 1:length(emojis)
println(i)
end
println("Iterating over the indices but printing value:")
for i โ eachindex(emojis)
println(emojis[i])
end
Iterating over each value:
๐ค
โ๏ธ
๐ป
Iterating over the indices:
1
2
3
Iterating over the indices but printing value:
๐ค
โ๏ธ
๐ป
Notice that we switched to using eachindex
instead of length
. Can you guess why?
break
and continue
If you need to stop a loop early the break
keyword will immediately terminate a loop.
If you need to skip an iteration the continue
keyword will immediately start the next iteration of the loop.
for i โ emojis
i == "โ๏ธ" && continue
println(i)
end
๐ค
๐ป
That covers most of the basic syntax we will need to get started.
Variables or names are part of a scope:
julia> x = 5
5
julia> foo(y, z) = x + y + z # x is available from global scope
Main.__FRANKLIN_1309937.foo
julia> foo(10, 20)
35
julia> bar(x, y) = x - y # x is "shadowed" by the argument
Main.__FRANKLIN_1309937.bar
julia> bar(10, 3)
7
julia> module Mod
x, y, z = (1, 2, 3)
foo(z) = println(x, y, z)
end
Main.__FRANKLIN_1309937.Mod
julia> import .Mod
julia> x = 50
50
julia> Mod.foo(10)
1210
Forgive the section title ๐
Julia has a fantastic built-in standard library for linear algebra.
using LinearAlgebra
Many languages have different concepts of arrays.
In a language like C you might use raw pointers ๐ฑ.
In a language like Python you might use NumPy arrays or PyTorch tensors
Types
This section will start talking a bit about types. If it's a bit confusing don't worry, the next section will clear up any confusion!
In Julia the array types stick pretty close to the mathematical sense of the word. The most common three array types in Julia are:
Vector{T} where T
Vector (alias for Array{T, 1} where T)
Matrix{T} where T
Matrix (alias for Array{T, 2} where T)
Array{T, N} where {T, N};
If this syntax is confusing to you don't worry! We'll talk more about this later but the expressions above are types. Specifically they are parametric types with a placeholder T
for the element type. The N
parameter for array is the dimension, which is an integer!
We can substitute different things such as types for the placeholder T
, for instance:
Vector{Int64}
Vector{Int64} (alias for Array{Int64, 1})
is a Vector
with 64-bit integer elements.
If we go a bit further we can create high dimensional array types:
Array{Bool, 6}
Array{Bool, 6}
Notice that Vector
and Matrix
are simply aliases for the a 1 dimensional and 2 dimensional Array
respectively
We've already seen literals several times!
[1,2,3]
3-element Vector{Int64}:
1
2
3
Here's some new literals!
["๐" "๐"
"๐ป" "๐ง"]
2ร2 Matrix{String}:
"๐" "๐"
"๐ป" "๐ง"
A = [1 2; 3 4]
2ร2 Matrix{Int64}:
1 2
3 4
There are lots of ways to write array literals in Julia. Check out your cheatsheet for more.
We have a funky syntax for constructing called comprehensions:
[x for x in 1:10 if x % 3 == 1]
4-element Vector{Int64}:
1
4
7
10
[i + j for i in 1:4, j in 1:3]
4ร3 Matrix{Int64}:
2 3 4
3 4 5
4 5 6
5 6 7
Every type in Julia has constructors, which are functions that return a specific type We have constructors for basic numeric types and strings:
Float64(10)
String("asdf")
"asdf"
The most basic array constructors:
Vector{Int64}(undef, 5) # 5 element Vector
5-element Vector{Int64}:
140505217305184
140505217305184
0
0
429578837520
Matrix{Float32}(undef, 2, 3)
2ร3 Matrix{Float32}:
3.63319f-13 8.0f-45 3.43128f-13
4.5842f-41 1.56f-43 4.5842f-41
Notice that the values are random. The undef
constructors, which use the special undef
global constant contain uninitialized memory whose values haven't been set to zero.
There is also special juxtaposition syntax for empty arrays:
Float32[]
Float32[]
zeros(Bool, 3)
3-element Vector{Bool}:
0
0
0
zeros(5, 5)
5ร5 Matrix{Float64}:
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.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
fill(1 + 10im, 2, 2)
2ร2 Matrix{Complex{Int64}}:
1+10im 1+10im
1+10im 1+10im
rand(Int8)
72
rand(5)
5-element Vector{Float64}:
0.6112199084494764
0.2928927487068307
0.11567450550036706
0.36317474803940897
0.10589270093375036
rand([:๐ฑ, :๐ถ, :๐ฆ, :๐ด])
:๐ถ
You can see above how we have used the rand
function for 3 completely different tasks above. We will talk more about this with methods
later.
v = collect(1:2:9)
5-element Vector{Int64}:
1
3
5
7
9
v[4] # 1-based indexing!
7
M = [1 2 3
4 5 6
7 8 9]
3ร3 Matrix{Int64}:
1 2 3
4 5 6
7 8 9
We can still index M
with a single integer:
M[5]
5
Can you guess what is happening above? This is called linear indexing
M[2, 3] # "cartesian" indexing
6
M[:, 1]
3-element Vector{Int64}:
1
4
7
M[2, :]
3-element Vector{Int64}:
4
5
6
The :
is a placeholder for the entire range of the specified dimension
M[[1, 3, 6]]
3-element Vector{Int64}:
1
7
8
bools = rand(Bool, length(v))
5-element Vector{Bool}:
1
0
1
0
0
v[bools]
2-element Vector{Int64}:
1
5
We can use the boolean version of indexing and the broadcasting we discussed earlier to filter!
v = rand(1:10, 8)
8-element Vector{Int64}:
3
4
10
3
8
6
6
6
v[v .> 5]
5-element Vector{Int64}:
10
8
6
6
6
Getting values from an array is very similar to setting them:
M[2, 2] = 10
10
But wait!!! Why is the value of the expression the RHS?
We can see that M
has been modified:
M
3ร3 Matrix{Int64}:
1 2 3
4 10 6
7 8 9
We can use all the same fancy indexing with a catch, if we refer to multiple indices on the left hand side we must use broadcasting!
M[:, 1] .= 2M[:, begin]
3-element view(::Matrix{Int64}, :, 1) with eltype Int64:
2
8
14
M
3ร3 Matrix{Int64}:
2 2 3
8 10 6
14 8 9
We can use the begin
and end
keywords instead of the 1st index (which might not be 1 ๐) and the last index in a dimension.
We get off easy with the broadcasting above since the shapes of the left hand side and right hand side obviously matched. What if they didn't?
M[1, 1:2] .= [1,2,3]
ERROR: DimensionMismatch: array could not be broadcast to match destination
Stacktrace:
[1] check_broadcast_shape
@ ./broadcast.jl:552 [inlined]
[2] check_broadcast_axes
@ ./broadcast.jl:555 [inlined]
[3] instantiate
@ ./broadcast.jl:310 [inlined]
[4] materialize!
@ ./broadcast.jl:883 [inlined]
[5] materialize!(dest::SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, UnitRange{Int64}}, true}, bc::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Nothing, typeof(identity), Tuple{Vector{Int64}}})
@ Base.Broadcast ./broadcast.jl:880
๐ฅ๐ฅ๐ฅ๐ฅ!
size(M)
(3, 3)
@show length(M) == size(M, 1) * size(M, 2)
length(M) == size(M, 1) * size(M, 2) = true
Reshape a 6 element range into a matrix, then flatten it into a vector
vec(reshape(collect(1:6), 2, 3))
6-element Vector{Int64}:
1
2
3
4
5
6
What happens if we take away the collect?
vec(reshape(1:6, 2, 3)) # we'll talk about this kind of trick later
1:6
Concatenating arrays is an important but tricky operation
vcat([1, 2, 3], [4, 5, 6])
6-element Vector{Int64}:
1
2
3
4
5
6
hcat([1,2,3], [4,5,6])
3ร2 Matrix{Int64}:
1 4
2 5
3 6
We can do more than just set specific indices of arrays (well, at least Vectors).
Julia's Vector
type can grow and shrink:
v = [] # Notice the type of `v`
Any[]
push!(v, :๐งก)
push!(v, :๐)
pushfirst!(v, :๐)
3-element Vector{Any}:
:๐
:๐งก
:๐
We've got a new convention to examine! In the Julia universe by convention (but not enforced by the compiler), functions which mutate one or more argument have !
as a suffix.
v
3-element Vector{Any}:
:๐
:๐งก
:๐
pop!(v)
:๐
v
2-element Vector{Any}:
:๐
:๐งก
We will be using lots of Unicode characters throughout the week. If you can't figure out how to type a character, but you're able to copy and paste it into the REPL you can use the help
REPL we discussed earlier:
help?> ๐ค