Skip to article frontmatterSkip to article content

Julia Code Organization

University of Central Florida
Valorum Data

Computational Analysis of Social Complexity

Fall 2025, 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
  • Now taht the module is defined I can call using Markov to make it possible to use any exported function without the Markov. prefix
using Main.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