Table of Contents

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.

https://julialang.org/

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