Learning Julia
I’ve been learning Julia recently for a class I’m taking, and I wanted to share kind of the order I learned things as well as some code snippets if you are getting into it.
Before we get started, the syntax is pretty similar to Python (like how it doesn’t use ;), but can be strongly typed if you want. It runs using a Just In Time (JIT) compiler, which means that the first time you run a function it will take a bit longer to compile, but subsequent runs will be much faster. When it compiles, some black magic happens and it generates optimized machine code that from my experience can be as fast as C or C++.
Julia REPL
The way we interact with Julia is through the REPL (Read-Eval-Print Loop). You can start it by running julia
in your terminal. If you want to stick to more of a script style, you can create a file with a .jl
extension and run it using julia filename.jl
.
Anyway when we start Julia, we see something like this:
[1m7.641s][~]$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.11.7 (2025-09-08)
_/ |\__'_|_|_|\__'_| |
|__/ |
julia>
Once you’re here, you can type Julia code and it will execute it immediately:
julia> println("Hello World!")
Hello World!
Basic Operations
Variables and Types
Getting started we will first talk about variables and types. You can declare a variable and assign it a value like so:
x = 10 # x is an Int
y::Float64 = 3.14 # y is a Float64
name = "Julia" # name is a String
is_active = true # is_active is a Bool
You can also make combinations of types using Union
:
z::Union{Int, Float64} = 5 # z can be either an Int or a Float64
In Julia, there is no null
like there is in some other languages. Instead, Julia uses nothing
to represent the absence of a value:
a = nothing
When checking equivalency with nothing
, you should use the ===
operator instead of ==
since nothing
is a singleton:
If you want to declare a variable with a type but know that it will sometimes be nothing
, you can use Union
with Nothing
:
b::Union{Int, Nothing} = nothing
b = 42
Another note is that people that write Julia tend to be into math, so you will see a lot of mathematical notation in the syntax. For example, you can use π
instead of pi
:
radius = 5.0
circumference = 2 * π * radius # Using π instead of pi
Or even set variable and function names and as Greek letters:
α = 10
β = 20
γ = α + β
μ(array) = sum(array) / length(array) # Mean function
σ(array) = sqrt(sum((x - μ(array))^2 for x in array) / length(array)) # Standard deviation function
arr = [1, 2, 3, 4, 5]
σ(arr) # Call the function
Arrays, Tuples, and Matrices
Arrays and tuples are just as easy to create:
arr = [1, 2, 3, 4, 5]
tup = (1, 2, 3)
arr_2::Array{Int64} = [1, 2, 3, 4, 5]
Do keep in mind however that arrays start at index 1 unlike most other programming languages where they start at index 0.
arr[1] # This would return 1
Julia also natively has support for matrices:
julia> mat = [1 2 3; 4 5 6; 7 8 9] # 3x3 Matrix
3×3 Matrix{Int64}:
1 2 3
4 5 6
7 8 9
Operations like matrix multiplication and transposition are also built in:
julia> mat' # Transpose of the matrix
3×3 adjoint(::Matrix{Int64}) with eltype Int64:
1 4 7
2 5 8
3 6 9
julia> mat * mat' # Matrix multiplication
3×3 Matrix{Int64}:
14 32 50
32 77 122
50 122 194
julia> vec = [1,2,3]
3-element Vector{Int64}:
1
2
3
julia> mat * vec
3-element Vector{Int64}:
14
32
50
There is a lot more you can do with arrays and matrices, but this should be a good starting point.
Printing to Console
If we want to print out values we can use the print
or println
function:
println("Hello, $(name)!") # String interpolation
println("x = $x, y = $y")
println("x + y = ")
print(x + y) # Note: print does not add a newline
This will output:
Hello, Julia!
x = 10, y = 3.14
x + y = 13.14
Operations
Some basic arithmetic operations:
a = 10 + 5 # Addition
b = 10 - 5 # Subtraction
c = 10 * 5 # Multiplication
d = 10 / 5 # Division
e = 10 % 3 # Modulus
f = 10 ^ 2 # Exponentiation
# and if you really want...
d_2 = 5\10 # yes you can do it the other way
@assert d == d_2 # true
Functions and Structs
Immutable vs Mutable
Functions in julia are often written in two styles: those that mutate their parameters, and those that don’t. Julia conventionally denotes functions that mutate their parameters with the !
symbol at the end of the function name. For example, sort!
sorts an array in place, mutating the original array, while sort
returns a new sorted array. Because of this naming convention, we see that string interpolation doesn’t let you interpolate the value var
like "$var!"
directly, you’d need to wrap it in parentheses, like "$(var)!"
.
arr = [3, 1, 2]
sorted_arr = sort(arr) # Immutable, returns a new sorted array
println(sorted_arr) # Output: [1, 2, 3]
sort!(arr) # Mutable, sorts the array in place
println(arr) # Output: [1, 2, 3]
Defining Functions
There are three general ways to define functions in Julia. The most common way is using the function
keyword:
function greet(name::String)
println("Hello, $(name)!")
end
greet("Julia") # Outputs: Hello, Julia!
If your function is simple, you can also use the one-liner syntax to accomplish the same thing:
greet(name::String) = println("Hello, $(name)!")
You can also create lambda expressions using the ->
syntax:
greet = name -> println("Hello, $(name)!")
You can use lambdas to do things like map, filter, and reduce on collections:
arr = [1, 2, 3, 4, 5]
squared = map(x -> x^2, arr) # Squares each element in arr
even_numbers = filter(x -> x % 2 == 0, arr) # Filters even numbers
sum_of_squares = reduce(+, squared) # Sums all elements in squared
This will output:
julia> arr = [1, 2, 3, 4, 5]
5-element Vector{Int64}:
1
2
3
4
5
julia> squared = map(x -> x^2, arr) # Squares each element in arr
5-element Vector{Int64}:
1
4
9
16
25
julia> even_numbers = filter(x -> x % 2 == 0, arr) # Filters even numbers
2-element Vector{Int64}:
2
4
julia> sum_of_squares = reduce(+, squared) # Sums all elements in squared
55
Using the Pipe Operator
Julia has a pipe operator |>
that allows you to chain function calls in a readable way. The value on the left side of the operator is passed as the first argument to the function on the right side.
For example, with our standard deviation function from earlier, we can use the pipe operator to pass an array through multiple functions:
arr = [1, 2, 3, 4, 5]
mean = arr |> μ # Calculate mean
std_dev = arr |> σ # Calculate standard deviation
# Equivalently, we could do:
mean = μ(arr)
std_dev = σ(arr)
This functional style can sometimes be more readable, especially when chaining multiple operations together in an intuitive way.
Defining Structs
Structs in Julia are similar to classes in other languages. They are used to create custom data types. By default, structs are immutable, but you can make them mutable by using the mutable struct
keyword.
struct Point
x::Float64
y::Float64
end
p1 = Point(1.0, 2.0)
println("Point p1: ($(p1.x), $(p1.y))") # Output: Point p1: (1.0, 2.0)
mutable struct MutablePoint
x::Float64
y::Float64
end
p2 = MutablePoint(3.0, 4.0)
println("Point p2 before: ($(p2.x), $(p2.y))") # Output: Point p2 before: (3.0, 4.0)
p2.x = 5.0 # Modify the x coordinate
println("Point p2 after: ($(p2.x), $(p2.y))") # Output: Point p2 after: (5.0, 4.0)
Control Flow
First off, Julia uses indentation for readability but does not enforce it like Python. Instead, it uses end
to denote the end of blocks.
Conditional Statements
Julia uses if
, elseif
, and else
for conditional statements:
x = 10
if x < 5
println("x is less than 5")
elseif x == 10
println("x is equal to 10")
else
println("x is greater than 5 and not equal to 10")
end
# will output "x is equal to 10"
Ternary Operator
Julia also supports a ternary operator for simple conditional assignments:
result = x < 5 ? "less than 5" : "5 or more"
println(result) # 5 or more
If you are familiar with other languages, this is a little different where the condition comes first, followed by the true case and then the false case.
Loops
Julia supports for
and while
loops:
# For loop
for i in 1:5 # 1 to 5 inclusive
println("Iteration $i")
end
# While loop
count = 1
while count <= 5
println("Count is $count")
count += 1 # note that count++ is not valid in Julia
end
Julia also supports for-each loops in a similar way to python:
arr = [10, 20, 30, 40, 50]
for value in arr
println("Value: $value") # will print the values 10, 20, 30, 40, 50
end
If we also want the index of the value, we can use the enumerate
function:
arr = [10, 20, 30, 40, 50]
for (index, value) in enumerate(arr)
println("Index: $index, Value: $value") # will print the index and value pairs
end
Comprehensions
Julia supports array comprehensions similar to that of python.
Generally the syntax is [expression for item in collection <if condition>]
where the if condition
is optional.
Here we can create an array of squares of even numbers from 1 to 10:
squares = [x^2 for x in 1:10 if x % 2 == 0] # Squares of even numbers from 1 to 10
println(squares)
This is a one liner that is equivalent to:
squares = []
for x in 1:10
if x % 2 == 0
push!(squares, x^2)
end
end
println(squares)
Multiple Dispatch
One of Julia’s most powerful features is multiple dispatch. This means a single function name can have many methods, and Julia will pick the most specific one that matches the runtime types of all arguments.
This looks a little like function overloading in languages such as Java, but it goes further: instead of only considering the first argument (the object the method is called on), Julia considers every argument in the call.
struct Rect
width::Float64
height::Float64
end
struct Circle
radius::Float64
end
area(rect::Rect) = rect.width * rect.height
area(circle::Circle) = π * circle.radius^2
Now the same function name, area, works for both rectangles and circles:
julia> area(Rect(3.0, 4.0))
12.0
julia> area(Circle(2.0))
12.566370614359172
Because dispatch looks at all arguments, you can also define clean, symmetric operations:
*(m::Matrix, v::Vector) = "Matrix–Vector multiplication"
*(v::Vector, m::Matrix) = "Vector–Matrix multiplication"
Multiple dispatch is one of the reasons Julia code feels both concise and extensible: new types can plug into existing functions naturally without modifying old code.
Broadcasting
Julia has a powerful feature called broadcasting, which allows you to apply functions element-wise to arrays and other collections. You can use the .
operator to broadcast a function over an array.
For example, if you want to add 1 to each element of an array, you can do:
arr = [1, 2, 3, 4, 5]
arr_plus_one = arr .+ 1 # Note the . before the +
println(arr_plus_one) # Output: [2, 3, 4, 5, 6]
You can also broadcast custom functions:
square(x) = x^2
squared_arr = square.(arr) # Note the . before the (
println(squared_arr) # Output: [1, 4, 9, 16, 25]
If there are multiple broadcasts within an expression, you can use the @.
macro to broadcast all functions in the expression:
arr = [1, 2, 3, 4, 5]
result = @. (arr + 1) * 2 # Adds 1 to each element and then multiplies by 2
println(result) # Output: [4, 6, 8, 10, 12]
# This is equivalent to:
result = (arr .+ 1) .* 2
println(result) # Output: [4, 6, 8, 10, 12]
Implementing a Stack
This is a simple implementation of a stack data structure using an immutable struct.
struct Stack{T}
value::T
next::Union{Stack{T}, Nothing}
end
function push(stack::Stack{T}, value::T) where T
return Stack{T}(value, stack)
end
function pop(stack::Stack{T}) where T
return stack.value, stack.next
end
function isEmpty(stack::Stack{T}) where T
return stack === nothing
end
function peek(stack::Stack{T}) where T
return stack.value
end
Package Management
Julia has a built-in package manager that you can access by typing ]
in the REPL. This will change the prompt to pkg>
. Alternatively, you can use the Pkg
module in your scripts.
To add a package, you can use the add
command:
pkg> add ExamplePackage
Or if you are not using the package manager part of the REPL, you can do it like this:
using Pkg
Pkg.add("ExamplePackage")
To use a package in your code, you can use the using
or import
keywords:
using ExamplePackage # Loads the package and brings its exported names into scope
import ExamplePackage # Loads the package but does not bring its names into scope
There are a lot of packages, ranging from Data Science (DataFrames.jl, CSV.jl) to Web Development (HTTP.jl, Genie.jl) to Machine Learning (Flux.jl, MLJ.jl).
BenchmarkTools
One of the most useful packages for performance testing is BenchmarkTools.jl
. You can install it using the package manager, adding BenchmarkTools
, and then use it to benchmark your functions:
using BenchmarkTools
@btime your_function(args...) # Replace with your function and arguments
@benchmark your_function(args...) # For more detailed output
Macros
Macros in Julia are a way to generate code programmatically. They are defined using the @
symbol. A common example is the @time
macro, which measures the execution time of an expression:
@time sleep(1) # Sleeps for 1 second and prints the time taken
This will output something like:
1.002045 seconds (4 allocations: 112 bytes)
You can also define your own macros using the macro
keyword:
macro sayhello(name)
return :(println("Hello, $(name)!"))
end
@sayhello("Julia") # Expands to println("Hello, Julia!")
This will output:
Hello, Julia!
Conclusion
This is just a basic overview of Julia. There is a lot more to explore, including advanced topics like multiple dispatch, metaprogramming, and performance optimization. I hope this helps you get started with Julia!
- Chris