Skip to article frontmatterSkip to article content

Julia Types and Methods

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

  • 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

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", x is a variable)
  • At its most basic level, a variable is composed of
    1. An arrangment of 0’s and 1’s called bits
    2. An address to where in memory the data is recorded
    3. A Symbol representing 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 of Any
  • At the bottom of the hierarchy is Union{} -- no objects are instances of Union{}
  • 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, and Int8). 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 is Any
    • Real is a special kind of Number and can be broken into two subgroups: AbstractFloat and Integer

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 isint to 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. UInt256 like 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 <: AbstractParentType is optional, as are types on all fields

Composite Types: Examples

struct Foo
   bar
   baz::Int
   qux::Float64
end
foo = 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.baz
23
foo.qux
1.5

Composite 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)
false
foo, isint(foo)
(Foo("Hello, world.", 23, 1.5), false)
isint(Foo(1, 23, 1.5))
true

Exercises

  1. Create an abstract type called Person
  2. Create two composite subtypes of Person called Friend and Foe
    • Each of these should have fields name and height_inches
    • For friend you should also have a field favorite_color
    • MAKE SURE TO ADD TYPES FOR ALL FIELDS
  3. Create a third composite subtype of Person called Stranger, but without any fields
  4. 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_party that implements that logic
    • HINT: you will need 4 methods. 3 of these have only one argument, the 4th has two
  5. 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 here
using 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
end
tests (generic function with 1 method)
# uncomment and run this cell when you are ready to test your code
tests()