Computational Analysis of Social Complexity
Fall 2026, Spencer Lyon
Prerequisites
Laptop or personal computer with internet connection
Julia intro lecture
Outcomes
Understand key components of Julia’s type system: abstract types, primitive types, composite types, and parametric types
Be able to define our own custom types to hold data
Understand the concept of multiple dispatch
Be able to leverage the mulitple dispatch system to define custom behavior for built-in and custom types
References
Lecture notes
Julia documentation on types and methods (these are technical, but comprehensive and well-written)
QuantEcon lectures on types and generic programming
Types in Julia¶
Julia is both very expressive and runtime efficient
This is made possible because of the underlying compiler technology
The main strategy for user interaction with the compiler is by defining custom types and methods that operate on those types
Types and multiple dispatch go hand in hand and are key to effective Julia
What is a type?¶
Each piece of data in a program resides in memory (RAM) on the host computer
We often assign names to data, which we call variables (in
x = "hello",xis a variable)At its most basic level, a variable is composed of
An arrangment of 0’s and 1’s called bits
An address to where in memory the data is recorded
A
Symbolrepresenting the name we gave the data
A type in Julia represents what kind of object is represented at a certain memory address
Julia uses this type information to enable syntax (e.g. the
$in a string to interpolate or the.access for an objects fields) and ultimiately decide what behaviors are defined to operate on the data
Organizing types¶
In Julia types are organized into a hierarchy
At the top of the hierarcy is
Any-- all objects are instances ofAnyAt the bottom of the hierarchy is
Union{}-- no objects are instances ofUnion{}In between these endpoints we have a rich family of types
Each type can have at most one parent type (if not specified, default parent is
Any)Types can actually come in a few different flavors...
Types of Types¶
Abstract Types: cannot be created directly, but serve as nodes in a type hierarchy. Help us organize types into families and provide shared behavior for all members of the family
Primitive types: provided to us by Julia and represent a collection of bits (e.g.
Float64,Bool, andInt8). We could create them, but we won’t. We won’t say anything else about them hereComposite Types: types that contain additional data called fields. An instance can be treated as a single value. This is what we typically define and use
NOTE: all objects in Julia are instances of either primitive or composite types, and can be related to one another by sharing common abstract type ancestors
Abstract Types¶
Abstract types help organize composite types into families
For example, the number system in Julia looks like this (really -- look here)
abstract type Number end
abstract type Real <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer <: Real end
abstract type Signed <: Integer end
abstract type Unsigned <: Integer endNote:
Number’s parent type isAnyRealis a special kind ofNumberand can be broken into two subgroups:AbstractFloatandInteger
Why Abstract Types?¶
We said before we can’t create an instance of abstract types...
So, why do we have them?
The primary reason to have abstract types is to introduced shared functionality via methods defined on the abstract type
Example: suppose you needed to define a function
isintto determine if an object is an integerWithout abstract types, you could have a long sequence of checks for if a variable is any integer type:
function isint1(x)
for T in [
Int8, UInt8, Int16, UInt16,
Int32, UInt32, Int64, UInt64,
Int128, UInt128
]
if isa(x, T)
return true
end
end
return false
end
isint1(10), isint1("Hello")(true, false)With abstract types we can define two methods:
isint(x) = false
isint(x::Integer) = true
isint(10), isint("Hello")(true, false)This has many benefits
Much simpler to write/reason about
More “fool proof”: what if we forgot one of the “UIntXX” types?
More “future proof”: what if a new type of integer gets introduced (e.g.
UInt256like is widely used in blockchain data!)Pushes work into the compiler:
@code_lowered isint1("hello")CodeInfo(
1 ─ %1 = Base.vect(Main.Int8, Main.UInt8, Main.Int16, Main.UInt16, Main.Int32, Main.UInt32, Main.Int64, Main.UInt64, Main.Int128, Main.UInt128)
│ @_3 = Base.iterate(%1)
│ %3 = @_3 === nothing
│ %4 = Base.not_int(%3)
└── goto #6 if not %4
2 ┄ %6 = @_3
│ T = Core.getfield(%6, 1)
│ %8 = Core.getfield(%6, 2)
│ %9 = x isa T
└── goto #4 if not %9
3 ─ return true
4 ─ @_3 = Base.iterate(%1, %8)
│ %13 = @_3 === nothing
│ %14 = Base.not_int(%13)
└── goto #6 if not %14
5 ─ goto #2
6 ┄ return false
)@code_lowered isint1(UInt128(12341234123423134))CodeInfo(
1 ─ %1 = Base.vect(Main.Int8, Main.UInt8, Main.Int16, Main.UInt16, Main.Int32, Main.UInt32, Main.Int64, Main.UInt64, Main.Int128, Main.UInt128)
│ @_3 = Base.iterate(%1)
│ %3 = @_3 === nothing
│ %4 = Base.not_int(%3)
└── goto #6 if not %4
2 ┄ %6 = @_3
│ T = Core.getfield(%6, 1)
│ %8 = Core.getfield(%6, 2)
│ %9 = x isa T
└── goto #4 if not %9
3 ─ return true
4 ─ @_3 = Base.iterate(%1, %8)
│ %13 = @_3 === nothing
│ %14 = Base.not_int(%13)
└── goto #6 if not %14
5 ─ goto #2
6 ┄ return false
)@code_lowered isint("hello") CodeInfo(
1 ─ return false
)@code_lowered isint(UInt128(12341234123423134))CodeInfo(
1 ─ return true
)Composite Types¶
Abstract types are very useful when used in conjunction with multiple dispatch (defining multiple methods of function with same name, but varying code depending on argument types)
However, most often we create types to hold collections of related data together
We do this using composite types
A composite type can be created as follows:
struct Name <: AbstractParentType
field1::Field1Type
# more fields
endNote that the
<: AbstractParentTypeis optional, as are types on all fields
Composite Types: Examples¶
struct Foo
bar
baz::Int
qux::Float64
endfoo = Foo("Hello, world.", 23, 1.5)Foo("Hello, world.", 23, 1.5)typeof(foo)Foo# this will not work. Uncomment and try it out
# Foo((), 23.5, 1)fieldnames(Foo)(:bar, :baz, :qux)foo.bar"Hello, world."foo.baz23foo.qux1.5Composite Types and Dispatch¶
Above we saw an example of defining multiple methods of
isint, using an abstract type to route dispatchWe can also use composite types
isint(x::Foo) = isint(x.bar)isint (generic function with 3 methods)isint(10), isint(1.0), isint(UInt128(234901324987213)), isint(Foo("not an int", 12, 1.0))(true, false, true, false)@code_lowered isint(Foo("not an int", 12, 1.0))CodeInfo(
1 ─ %1 = Base.getproperty(x, :bar)
│ %2 = Main.isint(%1)
└── return %2
)@code_lowered isint(UInt128(12341234123423134))CodeInfo(
1 ─ return true
)isint(foo)falsefoo, isint(foo)(Foo("Hello, world.", 23, 1.5), false)isint(Foo(1, 23, 1.5))trueExercises¶
Create an abstract type called
PersonCreate two composite subtypes of
PersoncalledFriendandFoeEach of these should have fields
nameandheight_inchesFor friend you should also have a field
favorite_colorMAKE SURE TO ADD TYPES FOR ALL FIELDS
Create a third composite subtype of
PersoncalledStranger, but without any fieldsSuppose we are trying to decide who to invite to a dinner party. Our rule is that friends should get a definite yes. Enemies a definite no. Strangers a 50%/50% toss up. However, if our spouse says we should invite a person, the answer is always yes
Create a function
should_invite_to_partythat implements that logicHINT: you will need 4 methods. 3 of these have only one argument, the 4th has two
In the cell at the bottom we have written a test case. You will know you’ve done this correctly when all the tests pass
# Your code hereusing Test
function tests()
@testset "people" begin
@test fieldnames(Friend) == (:name, :height_inches, :favorite_color)
@test fieldnames(Foe) == (:name, :height_inches)
@test fieldnames(Stranger) == tuple()
jim = Friend("Jim", 64, "blue")
dwight = Foe("Dwight", 61)
creed = Stranger()
@test jim isa Person
@test dwight isa Person
@test creed isa Person
@test should_invite_to_party(jim)
@test !should_invite_to_party(dwight)
@test should_invite_to_party(dwight, true)
creed_invites = map(i->should_invite_to_party(creed), 1:100)
@test any(creed_invites)
@test any(map(!, creed_invites))
creed_invites_spouse = map(i->should_invite_to_party(creed, true), 1:100)
@test all(creed_invites_spouse)
end
endtests (generic function with 1 method)# uncomment and run this cell when you are ready to test your code
# tests()