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
includefunctionExporting 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
.jlsource code filesModules
Packages
In this not we’ll learn how to manage .
jlfiles and ModulesWe’ll pick up with Packages in another lecture
.jl files¶
Julia source code typically lives in a plain text file with a
.jlextensionWhile 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]
endsimulate_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_dists2-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
1vals = simulate_values(mc1, 10)
vals10-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:
Copy/paste it to the new notebook
Store the code in a
.jlfile and import it from both places
Let’s pick the latter
include¶
I have created
markov.jlwith 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
includefunction 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.jlfile I added two extra functions (randandstationary_ditributions)We can verify that these were defined for us when we ran
includeby 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
.jlfile and load it in a notebook (or other.jlfile!) is already a huge win for organization and reusabilityBut 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, andstationary_distributionscode part of our session when loading our fileTo do this, we will need to organize our code into a Module
To create module we use the syntax
module NAME
# code here
endHere we use the
modulekeyword, followed by the name of the moduleBetween the module name and
endwe insert any code we want to include in the moduleBy 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:
I used the
exportkeyword to list types/functions I want to be added to the caller’s namespace when someone runsusing MarkovI used
includeto 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.MarkovNotice the printout shows that we now have a
Main.MarkovobjectNOTE:
Mainis 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
10Notice that I had to use
Markov.to access members of the moduleIn a fresh session I can call
using Markovto make it possible to use anyexported function without theMarkov.prefixBecause this notebook executes the earlier direct
include("markov.jl")in the same session, we keep theMarkov.prefix here so the function and argument use the same module’sMarkovChaintype
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
.jlfilesOne called
shapes.jl: here define the following:An abstract type
Shape2DConcrete types
Circle,Rectangle, andTrianglethat are subtypes ofShape2DMethods for
areaandperimeterfor each shape typeA function
is_inside(shape, x, y)that checks if a point (x, y) is inside the shape
One called
GeometricShapes.jl: here wrap the code fromshapes.jlin a module namedGeometricShapesExport the types:
Shape2D,Circle,Rectangle,TriangleExport the functions:
area,perimeter,is_inside
includeyour 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