Computational Analysis of Social Complexity
Fall 2025, 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 ofAny - At 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 here - Composite 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 end- Note:
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 integer- Without 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
end- Note 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 dispatch - We 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
Person - Create two composite subtypes of
PersoncalledFriendandFoe- Each of these should have fields
nameandheight_inches - For friend you should also have a field
favorite_color - MAKE SURE TO ADD TYPES FOR ALL FIELDS
- Each of these should have fields
- Create a third composite subtype of
PersoncalledStranger, but without any fields - Suppose 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 logic - HINT: you will need 4 methods. 3 of these have only one argument, the 4th has two
- Create a function
- 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()