Julia First Steps

The REPL

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.

Help mode (?)

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.

Examples

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.

Package mode (])

By pressing ] you access Pkg.jl, Julia's integrated package manager, whose documentation is an absolute must-read. Pkg.jl allows you to:

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.

Shell mode (;)

By pressing ; you enter a terminal, where you can execute any command you want. Here's an example for Unix systems:

Environments

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:

If 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.

Package Management

We're going to explore how package management and code structure typically works using this repository

Basic Syntax

Julia has a fairly simple syntax which is:

  1. "Close to the math" so that code resembles the equivalent algorithm in plain math or pseudocode. Unicode symbols can help get even closer to math notation.
  2. Avoids too much unique syntax / keywords in favor of macros
  3. Familiar (enough) to users of MATLAB or Python

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.

Loading packages

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

Literals and Variables

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

Operators and Functions

Like most languages Julia has two syntaxes for operating on values: operators and function calls.

Operators

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

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

Keyword and optional arguments

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

Functions are data

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

Anonymous functions

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)

Broadcasting

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

Control Flow

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:

Conditionals

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

Loops

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 Vectors, Tuples, StepRanges like 1:2:9, UnitRanges like 1:10 and more.

Instead of the in keyword we can also use = and โˆˆ (\in-TAB).

When we are looping over containers like Vectors 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.

A note on scope

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

Arrays!

Forgive the section title ๐Ÿ˜‡

Julia has a fantastic built-in standard library for linear algebra.

using LinearAlgebra

What is an "Array" in Julia?

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

Constructing arrays

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.

Comprehensions

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

Constructors

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[]

Functions that generate arrays:

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.

Indexing

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

Slicing

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

Fancy indexing

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

Setting values

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

๐Ÿ’ฅ๐Ÿ’ฅ๐Ÿ’ฅ๐Ÿ’ฅ!

A few important functions that on Arrays

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}:
 :๐Ÿ’™
 :๐Ÿงก

A bit of fun

Tips Tricks and Conventions

Can't figure out how to type a Unicode character?

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?> ๐Ÿค”

CC BY-SA 4.0 Raye Skye Kimmerer. Last modified: July 14, 2025.
Website built with Franklin.jl and the Julia programming language.