We encountered a few types in the last section, let's take a look in more detail
julia> using About
julia> about(1.0)
Float64 (<: AbstractFloat <: Real <: Number <: Any), occupies 8B.
0011111111110000000000000000000000000000000000000000000000000000
β¨ββββββ¬βββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
+ 2^0 Γ 1.000000000000000000
= 1.0000000000000000
julia> about(1)
Int64 (<: Signed <: Integer <: Real <: Number <: Any), occupies 8B.
0000000000000000000000000000000000000000000000000000000000000001
= +1
julia> about(true)
Bool (<: Integer <: Real <: Number <: Any), occupies 1B.
00000001 = true
julia> about((1,2, "asdf", "π "))
Tuple{Int64, Int64, String, String} (<: Any)
Memory footprint: 32B directly (referencing 56B in total)
1::Int64 8B 00000000000000000000 β¦ 00000000000000000001 1
2::Int64 8B 00000000000000000000 β¦ 00000000000000000010 2
3::String 8B @ 0x00007fd53ccd6798 "asdf"
4::String 8B @ 0x00007fd53ccd6818 "π "
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
8B 8B * *
* = Pointer (8B)
julia> about([1,2,3,4])
4-element Vector{Int64} (mutable) (<: DenseVector{Int64} <: AbstractVector{Int64} <: Any), occupies 24B directly (referencing 72B in total, holding 32B of data)
ref::MemoryRef{Int64} 16B Β«structΒ» MemoryRef{Int64}(Ptr β¦ 36a0), [1, 2, 3, 4])
size::Tuple{Int64} 8B Β«structΒ» (4,)
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
16B 8B
Int64 contents exist on the CPU within the ref::MemoryRef from 0x00007fd5476236a0 to 0x00007fd5476236c0.
ββββββββββββββββββββββββ β―(Γ 4)β― ββββββββββββββββββββββββββββββββ 4 items
000000010000000000000000 00000000000000000000000000000000 in
ββ0x01ββββ0x00ββββ0x00ββ β―(Γ25)β― ββ0x00ββββ0x00ββββ0x00ββββ0x00ββ 32 bytes
julia> about([1, 1.0, "Howdy"])
3-element Vector{Any} (mutable) (<: DenseVector{Any} <: AbstractVector{Any} <: Any), occupies 24B directly (referencing 93B in total)
ref::MemoryRef{Any} 16B Β«structΒ» MemoryRef{Any}(Ptr{No β¦ Any[1, 1.0, "Howdy"])
size::Tuple{Int64} 8B Β«structΒ» (3,)
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
16B 8B
Any contents exist on the CPU within the ref::MemoryRef from 0x00007fd53e1f1a00 to 0x00007fd53e1f1a18.
ββββββββββββββββββββββββ β―(Γ 3)β― ββββββββββββββββββββββββββββββββ 3 pointers
011000001001001110100011 11010101011111110000000000000000 in
ββ0x60ββββ0x93ββββ0xa3ββ β―(Γ17)β― ββ0xd5ββββ0x7fββββ0x00ββββ0x00ββ 24 bytes
julia> about(rand(100))
100-element Vector{Float64} (mutable) (<: DenseVector{Float64} <: AbstractVector{Float64} <: Any), occupies 24B directly (referencing 840B in total, holding 800B of data)
ref::MemoryRef{Float64} 16B Β«structΒ» MemoryRef{Float64}( β¦ .573433, 0.556942])
size::Tuple{Int64} 8B Β«structΒ» (100,)
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
16B 8B
Float64 contents exist on the CPU within the ref::MemoryRef from 0x00007fd53d7ba320 to 0x00007fd53d7ba640.
ββββββββββββββββββββββββ β―(Γ100)β― ββββββββββββββββββββββββββββββββ 100 items
111000000101111101110011 01110111110100101110000100111111 in
ββ0xe0ββββ0x5fββββ0x73ββ β―(Γ793)β― ββ0x77ββββ0xd2ββββ0xe1ββββ0x3fββ 800 bytes
Julia's type system is organized as a tree, with Any at the root and concrete types at the leaves.
We can use AbstractTrees.jl together with subtypes to walk this hierarchy and pretty-print it:
using AbstractTrees, InteractiveUtils
AbstractTrees.children(t::Type) = subtypes(t)
Now print_tree works on any type and renders the subtype hierarchy as a textual diagram.
Try it in your REPL:
julia> print_tree(Number)
Number
ββ MultiplicativeInverse
β ββ SignedMultiplicativeInverse
β ββ UnsignedMultiplicativeInverse
ββ Complex
ββ Real
ββ AbstractFloat
β ββ BigFloat
β ββ BFloat16
β ββ Float16
β ββ Float32
β ββ Float64
ββ AbstractIrrational
β ββ Irrational
ββ Integer
β ββ Bool
β ββ Signed
β β ββ BigInt
β β ββ Int128
β β ββ Int16
β β ββ Int32
β β ββ Int64
β β ββ Int8
β ββ Unsigned
β ββ UInt128
β ββ UInt16
β ββ UInt32
β ββ UInt64
β ββ UInt8
ββ Rational
The tree above gives you the shape. To poke around inside it, Julia has a small kit of
reflection tools β every one works on the same Number, Real, Integer, Float64, β¦ types you just saw.
The relations on the tree are <: (subtype) and >: (supertype):
julia> Int64 <: Integer
true
julia> Integer <: Number
true
julia> Float64 <: Integer
false
julia> Number >: Float64
true
isa is the value-level version (x isa T β‘ typeof(x) <: T):
julia> 1 isa Integer
true
julia> 1.0 isa Integer
false
julia> 1.0 isa Real
true
julia> (1, 2.0) isa Tuple
true
Walking up and down the tree:
julia> supertype(Int64)
Signed
julia> supertype(Signed)
Integer
julia> supertypes(Int64) # full chain to Any
(Int64, Signed, Integer, Real, Number, Any)
julia> subtypes(Integer)
Any[Bool, Signed, Unsigned]
julia> subtypes(AbstractFloat)
Any[BigFloat, Core.BFloat16, Float16, Float32, Float64]
Abstract vs. concrete β only concrete types can have instances:
julia> isabstracttype(Number)
true
julia> isabstracttype(Int64)
false
julia> isconcretetype(Int64)
true
julia> isconcretetype(Integer)
false
Two more handy ones for reasoning about overlap:
julia> typejoin(Int64, Float64) # smallest common supertype
Real
julia> typejoin(Int64, String)
Any
julia> typeintersect(Real, Integer) # largest common subtype
Integer
julia> typeintersect(Float64, Integer) # empty β disjoint branches
Union{}
Union{...} lets you talk about "any of these" as a single type:
julia> 1 isa Union{Int, Float64}
true
julia> "hi" isa Union{Int, Float64}
false
julia> Union{Int, Float64} <: Real # both branches are Real
true
Field introspection β handy once your own structs get involved (the Complex type
from the Number tree is a good live example):
julia> fieldnames(Complex{Float64})
(:re, :im)
julia> fieldtypes(Complex{Float64})
(Float64, Float64)
julia> fieldtype(Complex{Float64}, :re)
Float64
And to ask "what methods do I have that touch this type?":
julia> methodswith(Unsigned)
Method[rem(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:232, rem(x::Unsigned, y::Union{Int128, Int16, Int32, Int64, Int8}) @ Base int.jl:233, rem(x::Unsigned, ::Type{Signed}) @ Base int.jl:626, <<(x::Integer, c::Unsigned) @ Base operators.jl:712, >>>(x::Integer, c::Unsigned) @ Base operators.jl:793, ^(x::BigFloat, y::Unsigned) @ Base.MPFR mpfr.jl:816, abs(x::Unsigned) @ Base int.jl:187, cld(x::Signed, y::Unsigned) @ Base div.jl:350, cld(x::Unsigned, y::Signed) @ Base div.jl:351, div(x::Signed, y::Unsigned, ::RoundingMode{:Up}) @ Base div.jl:304, div(x::Signed, y::Unsigned, ::RoundingMode{:Down}) @ Base div.jl:295, div(x::Unsigned, y::Signed, ::RoundingMode{:Up}) @ Base div.jl:308, div(x::Unsigned, y::Signed, ::RoundingMode{:Down}) @ Base div.jl:299, div(x::T, y::T, ::RoundingMode{:Down}) where T<:Unsigned @ Base div.jl:368, div(x::T, y::T, ::RoundingMode{:Up}) where T<:Unsigned @ Base div.jl:375, div(x::Unsigned, y::Union{Int128, Int16, Int32, Int64, Int8}) @ Base int.jl:230, div(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:229, divrem(x::Unsigned, y::Union{Int128, Int16, Int32, Int64, Int8}) @ Base int.jl:240, divrem(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:235, fld(x::Signed, y::Unsigned) @ Base div.jl:348, fld(x::Unsigned, y::Signed) @ Base div.jl:349, gcd(a::Signed, b::Unsigned) @ Base intfuncs.jl:150, gcd(a::Unsigned, b::Signed) @ Base intfuncs.jl:149, gcdx(a::Signed, b::Unsigned) @ Base intfuncs.jl:257, gcdx(a::Unsigned, b::Signed) @ Base intfuncs.jl:263, isvalid(::Type{<:AbstractChar}, c::Unsigned) @ Base.Unicode strings/unicode.jl:59, lcm(a::Signed, b::Unsigned) @ Base intfuncs.jl:152, lcm(a::Unsigned, b::Signed) @ Base intfuncs.jl:151, mod(x::T, y::T) where T<:Unsigned @ Base int.jl:297, mod(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:289, mod(x::Unsigned, y::Signed) @ Base int.jl:293, print(io::IO, n::Unsigned) @ Base show.jl:1287, show(io::IO, n::Unsigned) @ Base show.jl:1286, sign(x::Unsigned) @ Base number.jl:163, signbit(x::Unsigned) @ Base int.jl:140, widemul(x::Unsigned, y::Signed) @ Base int.jl:841, widemul(x::Signed, y::Unsigned) @ Base int.jl:840]
Advanced: Base.isbitstype(T) tells you whether T is plain old data (stack-allocatable, no pointers) β this is the property that lets values like Int64 and Float64 sit inline inside arrays without any indirection.
julia> isbits(1), isbits(1.0), isbits("hi")
(true, true, false)
struct (without mutable) is the default β once constructed, fields can't be rebound:
julia> struct Point
x::Float64
y::Float64
end
julia> p = Point(1.0, 2.0)
Main.__FRANKLIN_1181134.Point(1.0, 2.0)
julia> p.x
1.0
julia> typeof(p)
Main.__FRANKLIN_1181134.Point
Trying to mutate a field is an error:
julia> try
p.x = 5.0
catch e
e
end
ErrorException("setfield!: immutable struct of type Point cannot be changed")
Warning: "Immutable" means the fields can't be rebound. If a field happens to hold a mutable value (like a Vector), the contents of that value are still mutable β only the binding is frozen.
Prepend mutable if you need to reassign fields after construction:
julia> mutable struct Counter
n::Int
end
julia> c = Counter(0)
Main.__FRANKLIN_1181134.Counter(0)
julia> c.n += 1
1
julia> c.n += 1
2
julia> c
Main.__FRANKLIN_1181134.Counter(2)
Mutables are heap-allocated and compared by identity (===) by default, where immutables are compared by value:
julia> Point(1.0, 2.0) == Point(1.0, 2.0)
true
julia> Counter(0) == Counter(0)
false
A type parameter lets one struct cover many element types β and lets the compiler specialise:
julia> struct Pair2{T}
x::T
y::T
end
julia> Pair2(1, 2) # Pair2{Int}
Main.__FRANKLIN_1181134.Pair2{Int64}(1, 2)
julia> Pair2(1.0, 2.0) # Pair2{Float64}
Main.__FRANKLIN_1181134.Pair2{Float64}(1.0, 2.0)
julia> typeof(Pair2(1, 2))
Main.__FRANKLIN_1181134.Pair2{Int64}
You can constrain the parameter with <::
julia> struct NumPair{T<:Number}
x::T
y::T
end
julia> NumPair(1, 2)
Main.__FRANKLIN_1181134.NumPair{Int64}(1, 2)
julia> try
NumPair("a", "b")
catch e
e
end
MethodError(Main.__FRANKLIN_1181134.NumPair, ("a", "b"), 0x0000000000009918)
abstract type declares a node in the type tree β no fields, no constructor, just a label other types can sit under:
julia> abstract type Animal end
julia> struct Dog <: Animal
name::String
end
julia> struct Cat <: Animal
name::String
end
julia> Dog("Rex") isa Animal, Cat("Mia") isa Animal
(true, true)
Methods written against the abstract type apply to every concrete subtype:
julia> speak(a::Animal) = "$(a.name) makes a sound"
Main.__FRANKLIN_1181134.var"#speak"()
julia> speak(d::Dog) = "$(d.name) says woof"
Main.__FRANKLIN_1181134.var"#speak"()
julia> speak(Dog("Rex"))
Rex says woof
julia> speak(Cat("Mia"))
Mia makes a sound
A struct with no fields is its own type β useful as a dispatch tag (this is exactly what the rock-paper-scissors example below uses):
julia> struct Marker end
julia> Marker() === Marker()
true
By default, every struct gets one outer constructor matching its field list. You can add more outer constructors to provide defaults or conversions:
julia> struct Celsius
value::Float64
end
julia> # Outer constructor: build a Celsius from a Fahrenheit number
julia> Celsius(Β°f::Int) = Celsius((Β°f - 32) * 5 / 9)
Main.__FRANKLIN_1181134.Celsius
julia> Celsius(0) # 0 Β°F as Β°C
Main.__FRANKLIN_1181134.Celsius(-17.77777777777778)
julia> Celsius(100.0) # already Β°C
Main.__FRANKLIN_1181134.Celsius(100.0)
An inner constructor lives inside the struct block. It's the only way to enforce invariants β you call new(...) instead of recursing into the type, and the default constructor is suppressed once you write one:
julia> struct PositiveInt
n::Int
function PositiveInt(n::Int)
n > 0 || throw(ArgumentError("expected positive, got $n"))
new(n)
end
end
julia> PositiveInt(5)
Main.__FRANKLIN_1181134.PositiveInt(5)
julia> try
PositiveInt(-1)
catch e
e
end
ArgumentError("expected positive, got -1")
Advanced: Parametric inner constructors use new{T}(...), and you'll often write both a "constrained" inner ctor (enforcing the invariant) and a thin outer ctor (inferring the parameter from the arguments).
A textual tree shows the type hierarchy, but it doesn't show which method Julia would actually call for a given combination of argument types. For that we can use DispatchDisplay.jl, which renders a generic function as a grid coloured by the method dispatch would select for each cell.
Following Mosè Giordano's
classic post,
let's encode rock-paper-scissors as multiple dispatch. play dispatches on
the types Type{Rock}, Type{Paper}, β¦ β each explicit win-rule is its own
method:
julia> abstract type Shape end
julia> struct Rock <: Shape end
julia> struct Paper <: Shape end
julia> struct Scissors <: Shape end
julia> play(::Type{Paper}, ::Type{Rock}) = "Paper wins"
Main.__FRANKLIN_1181134.var"#play"()
julia> play(::Type{Paper}, ::Type{Scissors}) = "Scissors wins"
Main.__FRANKLIN_1181134.var"#play"()
julia> play(::Type{Rock}, ::Type{Scissors}) = "Rock wins"
Main.__FRANKLIN_1181134.var"#play"()
julia> play(::Type{T}, ::Type{T}) where {T<:Shape} = "Tie, try again"
Main.__FRANKLIN_1181134.var"#play"()
julia> play(a::Type{<:Shape}, b::Type{<:Shape}) = play(b, a) # commutativity fallback
Main.__FRANKLIN_1181134.var"#play"()
A couple of spot-checks at the REPL:
julia> play(Type{Paper}, Type{Rock})
ERROR: MethodError: no method matching play(::Type{Type{Main.__FRANKLIN_1181134.Paper}}, ::Type{Type{Main.__FRANKLIN_1181134.Rock}})
The function `play` exists, but no method is defined for this combination of argument types.
Closest candidates are:
play(!Matched::Type{Main.__FRANKLIN_1181134.Rock}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
@ [Franklin]:1
play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
@ [Franklin]:1
play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Rock})
@ [Franklin]:1
...
Stacktrace:
julia> play(Type{Scissors}, Type{Paper})
ERROR: MethodError: no method matching play(::Type{Type{Main.__FRANKLIN_1181134.Scissors}}, ::Type{Type{Main.__FRANKLIN_1181134.Paper}})
The function `play` exists, but no method is defined for this combination of argument types.
Closest candidates are:
play(!Matched::Type{Main.__FRANKLIN_1181134.Rock}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
@ [Franklin]:1
play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
@ [Franklin]:1
play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Rock})
@ [Franklin]:1
...
Stacktrace:
julia> play(Type{Rock}, Type{Rock})
ERROR: MethodError: no method matching play(::Type{Type{Main.__FRANKLIN_1181134.Rock}}, ::Type{Type{Main.__FRANKLIN_1181134.Rock}})
The function `play` exists, but no method is defined for this combination of argument types.
Closest candidates are:
play(!Matched::Type{Main.__FRANKLIN_1181134.Rock}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
@ [Franklin]:1
play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
@ [Franklin]:1
play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Rock})
@ [Franklin]:1
...
Stacktrace:
Now, in your own REPL, load an interactive Makie backend together with DispatchDisplay and render the 3Γ3 dispatch grid (the lines below are copy-paste-only β they need a live OpenGL window so the static site doesn't run them):
using GLMakie # interactive window β best for hovering / 3D
using DispatchDisplay
shapes3 = [Type{Rock}, Type{Paper}, Type{Scissors}]
d3 = dispatchdisplay(play, shapes3, shapes3)
The grid makes the whole game visible at a glance:
Tie method, one colour spread across all three
same-vs-same matchups.play(a, b) = play(b, a) β same colour as that single method.Add a Well that beats Rock and Scissors but loses to Paper:
julia> struct Well <: Shape end
julia> play(::Type{Well}, ::Type{Rock}) = "Well wins"
Main.__FRANKLIN_1181134.play
julia> play(::Type{Well}, ::Type{Scissors}) = "Well wins"
Main.__FRANKLIN_1181134.play
julia> play(::Type{Well}, ::Type{Paper}) = "Paper wins"
Main.__FRANKLIN_1181134.play
Then re-render with a 4-way axis (again, copy into your REPL β needs GLMakie):
shapes4 = [Type{Rock}, Type{Paper}, Type{Scissors}, Type{Well}]
d4 = dispatchdisplay(play, shapes4, shapes4)
The new row/column is mostly Well-wins, with one Paper-wins cell β and the commutativity fallback fills in the lower triangle for free.