Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Julia Code Organization

University of Central Florida
Valorum Data

Computational Analysis of Social Complexity

Fall 2026, Spencer Lyon

Prerequisites

  • Laptop or personal computer with internet connection

  • Julia intro lecture

Outcomes

  • Creating julia modules

  • Importing Julia source fode files with the include function

  • Exporting types and functions to create

References

  • Lecture notes

  • Julia documentation on modules

Introduction

  • Julia can be succesfully used for exploratory analysis and one-off scripts

  • As projects grow, it is often useful to add structure and organization to the code

  • In Julia, the main building blocks for code organization and re-use include

    • Types/structs

    • Functions/methods

    • .jl source code files

    • Modules

    • Packages

  • In this not we’ll learn how to manage .jl files and Modules

  • We’ll pick up with Packages in another lecture

.jl files

  • Julia source code typically lives in a plain text file with a .jl extension

  • While the extension is optional and not enforced by the Julia REPL or runtime, it is a strong convention followed by the community and by 3rd party tools like code editors and GitHub

  • Suppose we have the following code for simulating Markov Chains in Julia

# Markov Chain code

struct MarkovChain{T}
    P::Matrix{Float64}
    initial_state::Vector{Float64}
    state_values::Vector{T}

    P_dists::Vector{Vector{Float64}}
end

function MarkovChain(P::Matrix{Float64}, initial_state::Vector{Float64}, state_values::Vector{T}) where T
    P_dists = [cumsum(row) for row in eachrow(P)]
    return MarkovChain{T}(P, initial_state, state_values, P_dists)
end

function simulate_indices(mc::MarkovChain, n_steps::Int)
    init_dist = cumsum(mc.initial_state)
    states = Vector{Int}(undef, n_steps)
    states[1] = searchsortedfirst(init_dist, rand())
    for i in 2:n_steps
        states[i] = searchsortedfirst(mc.P_dists[states[i-1]], rand())
    end
    return states
end

function simulate_values(mc::MarkovChain{T}, n_steps::Int)::Vector{T} where T
    states = simulate_indices(mc, n_steps)
    return mc.state_values[states]
end
simulate_values (generic function with 1 method)
P1 = [0.5 0.5; 0.5 0.5]
mc1 = MarkovChain(P1, [1.0, 0.0], ["A", "B"])

typeof(mc1)
MarkovChain{String}
mc1.P_dists
2-element Vector{Vector{Float64}}: [0.5, 1.0] [0.5, 1.0]
inds = simulate_indices(mc1, 10)
10-element Vector{Int64}: 1 1 2 2 1 1 1 2 1 1
vals = simulate_values(mc1, 10)
vals
10-element Vector{String}: "A" "A" "A" "A" "A" "B" "A" "A" "B" "A"
  • As you can see, we can use this code from inside a Jupyter notebook by running the defining cell and then calling the routines

  • However, what if we wanted to reuse the code for another notebook

  • We have two options:

    1. Copy/paste it to the new notebook

    2. Store the code in a .jl file and import it from both places

  • Let’s pick the latter

include

  • I have created markov.jl with that code:

println(String(read("markov.jl")))
# Markov Chain code

struct MarkovChain{T}
    P::Matrix{Float64}
    initial_state::Vector{Float64}
    state_values::Vector{T}

    P_dists::Vector{Vector{Float64}}
end

function MarkovChain(P::Matrix{Float64}, initial_state::Vector{Float64}, state_values::Vector{T}) where T
    P_dists = [cumsum(row) for row in eachrow(P)]
    return MarkovChain{T}(P, initial_state, state_values, P_dists)
end

function simulate_indices(mc::MarkovChain, n_steps::Int)
    init_dist = cumsum(mc.initial_state)
    states = Vector{Int}(undef, n_steps)
    states[1] = searchsortedfirst(init_dist, rand())
    for i in 2:n_steps
        states[i] = searchsortedfirst(mc.P_dists[states[i-1]], rand())
    end
    return states
end

function simulate_values(mc::MarkovChain{T}, n_steps::Int)::Vector{T} where T
    states = simulate_indices(mc, n_steps)
    return mc.state_values[states]
end

function Base.rand(mc::MarkovChain{T}, n_steps::Int)::Vector{T} where T
    simulate_values(mc, n_steps)
end

function stationary_distributions(mc::MarkovChain)
    eig = eigen(mc.P')
    out = Vector{Float64}[]
    for i in 1:size(mc.P, 1)
        if eig.values[i] == 1
            vec = eig.vectors[:, i]
            vec = vec ./ sum(vec)
            push!(out, vec)
        end
    end
    return out
end

  • To make use of this code in julia I can run include("markov.jl")

  • You can think of the include function as copy/pasting AND evaluating the code from a file in whatever setting you are in

include("markov.jl")
stationary_distributions (generic function with 1 method)
  • Notice that in the markov.jl file I added two extra functions (rand and stationary_ditributions)

  • We can verify that these were defined for us when we ran include by calling them:

rand(mc1, 4)
4-element Vector{String}: "A" "B" "B" "A"
stationary_distributions(mc1)
1-element Vector{Vector{Float64}}: [0.5, 0.5]

Modules

  • Being able to define code in a .jl file and load it in a notebook (or other .jl file!) is already a huge win for organization and reusability

  • But we can do better!

  • One issue with our approach here is that all types, functions, and methods we define in our included files will become part of our working Julia session.

  • This is not always wanted

  • Suppose instead we really only wanted to make the MarkovChain, rand, and stationary_distributions code part of our session when loading our file

  • To do this, we will need to organize our code into a Module

  • To create module we use the syntax

module NAME

# code here

end
  • Here we use the module keyword, followed by the name of the module

  • Between the module name and end we insert any code we want to include in the module

  • By convention, the contents of a module are not indented (this is a rare exception to indenting code that comes before end)

  • I’ve created a module for our Markov chain code in module.jl:

println(String(read("module.jl")))
module Markov

export MarkovChain, rand, stationary_distributions

include("markov.jl")

end

  • Notice a few things:

    1. I used the export keyword to list types/functions I want to be added to the caller’s namespace when someone runs using Markov

    2. I used include to add the actual source code in the module

  • This is a very common pattern in Julia and is one we will see throughout our course

Using Modules

  • To use a module we first have to evaluate the code defining it

  • To do that we can include("module.jl")

include("module.jl")
Main.Markov
  • Notice the printout shows that we now have a Main.Markov object

    • NOTE: Main is the name of the default module the user executed code is evaluated in to

  • We can now access Markov.<NAME> where <NAME> is any type or function in the module

P2 = [0.2 0.8; 0.5 0.5]
mc2 = Markov.MarkovChain(P2, [1.0, 0.0], [10, 20])
Main.Markov.MarkovChain{Int64}([0.2 0.8; 0.5 0.5], [1.0, 0.0], [10, 20], [[0.2, 1.0], [0.5, 1.0]])
Markov.rand(mc2, 2)
2-element Vector{Int64}: 10 10
  • Notice that I had to use Markov. to access members of the module

  • In a fresh session I can call using Markov to make it possible to use any exported function without the Markov. prefix

  • Because this notebook executes the earlier direct include("markov.jl") in the same session, we keep the Markov. prefix here so the function and argument use the same module’s MarkovChain type

using Main.Markov
Markov.stationary_distributions(mc2)
1-element Vector{Vector{Float64}}: [0.38461538461538464, 0.6153846153846153]

Exercise

  • Now it is your turn

  • Let’s create a module for working with geometric shapes

  • Create two new .jl files

    1. One called shapes.jl: here define the following:

      • An abstract type Shape2D

      • Concrete types Circle, Rectangle, and Triangle that are subtypes of Shape2D

      • Methods for area and perimeter for each shape type

      • A function is_inside(shape, x, y) that checks if a point (x, y) is inside the shape

    2. One called GeometricShapes.jl: here wrap the code from shapes.jl in a module named GeometricShapes

      • Export the types: Shape2D, Circle, Rectangle, Triangle

      • Export the functions: area, perimeter, is_inside

  • include your new module and test it by:

    • Creating instances of each shape

    • Computing their areas and perimeters

    • Testing if various points are inside or outside the shapes