using AbstractTrees
julia> typeof(3.14159265)
Float64
julia> typeof(π)
Irrational{:π}
julia> typeof(rand(3))
Vector{Float64}
julia> typeof([1, 1.5, true]) # What happened here?
Vector{Float64}
julia> typeof(1.5 + 1//3 .- [1, 2, 3])
Vector{Float64}
Some languages such as Java have a distinction between certain primitive types like Int64
and classes. While Julia does have primitive and non-primitive types:
julia> isprimitivetype(Int64)
true
julia> isprimitivetype(Vector{String})
false
The user experiences no difference, the distinction is purely for bootstrapping.
julia> x = 1.0
1.0
julia> typeof(x)
Float64
julia> x = [1,2,3]
[1, 2, 3]
julia> typeof(x)
Vector{Int64}
julia> typeof(sin)
typeof(sin)
julia> typeof((x, y) -> x + y^2) # Why does this look so weird?
Main.__FRANKLIN_1273372.var"#1#2"
The most common place you will see types is constructors:
julia> Int128(3)
3
julia> Vector{Float64}(undef, 2)
[6.9419223265881e-310, 6.94192498419883e-310]
julia> Complex{Float64}(1.0, 3.0)
1.0 + 3.0im
and in type assertions:
julia> 3.0::Float64
3.0
julia> 3.5::Int64
ERROR: TypeError: in typeassert, expected Int64, got a value of type Float64
Stacktrace:
julia> (1.5 + 10)::Float64
11.5
Important - This syntax is central to Julia programming
The type assertion syntax ::T
is used everywhere from struct definitions to Julia's all-important multiple-dispatch system which we will cover shortly. It is important to get comfortable with the meaning of this syntax
You can read ::Float64
as "is an instance of Float64
". A type assertion ensures or states that the left hand side is a subtype of the right hand side. What does that mean?
You may know about type inheritance from a language like Java or C++, or even Python. In those languages you can create a type (classes in some languages), and then create a second type that inherits the fields and behavior of the parent.
Julia on the other has a tree of abstract types (which contain no structure), and what we call concrete types on the leaves:
AbstractTrees.children(x::Type) = subtypes(x)
# All of the subtypes of the abstract type Number in a tree
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
Let's explore this type tree a little bit. At the top we have:
help?> Number
Number
Abstract supertype for all number types.
Since Number
is not a leaf on our tree it is an abstract type. A fragment of this type tree would be defined as follows:
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
abstract type
should be fairly self explanatory, but we have a new operator! The <:
subtype operator. Number
has an implicit supertype here:
supertype(Number)
Any
help?> Any
Any::DataType
Any
is the union of all types. It has the defining property isa(x, Any) == true
for any x
. Any
therefore describes the entire universe of possible values. For example Integer
is a subset of Any
that includes Int
, Int8
, and other integer types.
We have already seen Any
many times without realizing it:
foo(a, b) = 2a + 3b
is equivalent to:
foo(a::Any, b::Any) = 2a + 3b
Immediately you should notice that we might specialize foo
on some subtypes of Any
.
We've made do thus far with types defined by Julia, it's time we give it a shot!
One of the simplest types we might create is a Point
consisting of two values x
and y
:
struct Point1
x
y
end
julia> p = Point1(19, 97) # construct a Point1
Main.__FRANKLIN_1273372.Point1(19, 97)
julia> p.x # access a field of p
19
julia> p2 = Point1("North", [1, 2, 3, 4])
Main.__FRANKLIN_1273372.Point1("North", [1, 2, 3, 4])
julia> p.y
97
struct
struct
are immutable:
julia> p.x = 2
ERROR: setfield!: immutable struct of type Point1 cannot be changed
Stacktrace:
[1] setproperty!(x::Main.__FRANKLIN_1273372.Point1, f::Symbol, v::Int64)
@ Base ./Base.jl:53
julia> p.y[1] = 10 # but not necessarily their fields
ERROR: MethodError: no method matching setindex!(::Int64, ::Int64, ::Int64)
The function `setindex!` exists, but no method is defined for this combination of argument types.
Stacktrace:
julia> p
Main.__FRANKLIN_1273372.Point1(19, 97)
julia>
julia> Point1(1, 2) === Point1(1, 2)
true
julia>
help?> ===
===(x,y) -> Bool
≡(x,y) -> Bool
Determine whether x
and y
are identical, in the sense that no program could distinguish them. First the types of x
and y
are compared. If those are identical, mutable objects are compared by address in memory and immutable objects (such as numbers) are compared by contents at the bit level. This function is sometimes called "egal". It always returns a Bool
value.
julia> a = [1, 2]; b = [1, 2];
julia> a == b
true
julia> a === b
false
julia> a === a
true
Point1
be bad?Implicitly the fields x
and y
are of type Any
:
struct Point1
x::Any
y::Any
end
Important - p.x
and p.y
retained their type
despite the fact that the fields of Point1
are Any
. This is a fundamental feature of the language, that values never change type. However we will see that since the compiler will have trouble predicting the value of those fields ahead of time it must be pessimistic
We can instead restrict the types of each field by either concrete or abstract types:
struct Point2
x::Float64
y::Number
end
julia> Point2(1.5, 3)
Main.__FRANKLIN_1273372.Point2(1.5, 3)
julia> Point2([1,2,3], 4)
ERROR: MethodError: Cannot `convert` an object of type Vector{Int64} to an object of type Float64
The function `convert` exists, but no method is defined for this combination of argument types.
Closest candidates are:
convert(::Type{T}, !Matched::T) where T<:Number
@ Base number.jl:6
convert(::Type{T}, !Matched::Number) where T<:Number
@ Base number.jl:7
convert(::Type{T}, !Matched::T) where T
@ Base Base.jl:126
...
Stacktrace:
[1] Main.__FRANKLIN_1273372.Point2(x::Vector{Int64}, y::Int64)
@ Main.__FRANKLIN_1273372 ./string:2
Mutable types are created in a similar manner:
mutable struct Point3 <: Any
x::Float64
y::ComplexF64
end
But they differ in two ways:
julia> p = Point3(1.0, 10 + 1im)
Main.__FRANKLIN_1273372.Point3(1.0, 10.0 + 1.0im)
julia> p.x = 10
10
julia> p
Main.__FRANKLIN_1273372.Point3(10.0, 10.0 + 1.0im)
julia> p2 = Point3(10, 10 + 1im)
Main.__FRANKLIN_1273372.Point3(10.0, 10.0 + 1.0im)
julia> p == p2 # we can write a new method for == to make this true
false
julia> p === p2 # we cannot make this true
false
julia>
p
can change over time it can only be identified by it's address in memory. Hence types declared as mutable struct
are typically stored with a stable address in memory.struct
or a mutable struct
?struct
if:Vector
smutable struct
if:Our first Point
struct had fields which could contain any value. That freedom is great, and Julia happily accepts this level of dynamism. But we have another often better solution with parametrization.
We have already seen parametric types before:
mutable struct Vector{T} <: DenseVector{T}
ref::MemoryRef{T}
size::Tuple{Int64}
end
mutable struct Matrix{T} <: DenseMatrix{T}
ref::MemoryRef{T}
size::Tuple{Int64, Int64}
end
mutable struct Array{T, N} <: DenseArray{T, N}
ref::MemoryRef{T}
size::NTuple{N, Int64}
end
Let's take a look at the type tree they are part of:
print_tree(AbstractArray)
AbstractArray
├─ AbstractRange
│ ├─ LinRange
│ ├─ OrdinalRange
│ │ ├─ AbstractUnitRange
│ │ │ ├─ IdentityUnitRange
│ │ │ ├─ OneTo
│ │ │ ├─ Slice
│ │ │ └─ UnitRange
│ │ └─ StepRange
│ └─ StepRangeLen
├─ AbstractSlices
│ └─ Slices
├─ ExceptionStack
├─ LogRange
├─ LogicalIndex
├─ MethodList
├─ ReinterpretArray
├─ ReshapedArray
├─ SCartesianIndices2
├─ WithoutMissingVector
├─ BitArray
├─ CartesianIndices
├─ AbstractRange
│ ├─ LinRange
│ ├─ OrdinalRange
│ │ ├─ AbstractUnitRange
│ │ │ ├─ IdentityUnitRange
│ │ │ ├─ OneTo
│ │ │ ├─ Slice
│ │ │ ├─ StmtRange
│ │ │ └─ UnitRange
│ │ └─ StepRange
│ └─ StepRangeLen
├─ BitArray
├─ ExceptionStack
├─ LinearIndices
├─ LogRange
├─ MethodList
├─ TwoPhaseDefUseMap
├─ TwoPhaseVectorView
├─ DenseArray
│ ├─ Array
│ ├─ CodeUnits
│ ├─ Const
│ ├─ GenericMemory
│ └─ UnsafeView
├─ AbstractTriangular
│ ├─ LowerTriangular
│ ├─ UnitLowerTriangular
│ ├─ UnitUpperTriangular
│ └─ UpperTriangular
├─ Adjoint
├─ Bidiagonal
├─ Diagonal
├─ Hermitian
├─ SymTridiagonal
├─ Symmetric
├─ Transpose
├─ Tridiagonal
├─ UpperHessenberg
├─ LinearIndices
├─ PermutedDimsArray
├─ SubArray
└─ GenericArray
The type tree for AbstractArray
is massive, and that's with very few additional packages loaded which may add new subtypes. But it's deceptively small. We know that at least three of the types in this tree are parametric, and each one of those contains a potentially infinite subtree of subtypes.
So let's make our own infinite tree of subtypes:
struct Point{T}
x::T
y::T
end
Play around with substituting various types, and let's examine the subtyping relationships which are a little bit subtle!
julia> Point{Complex{Float64}}
Main.__FRANKLIN_1273372.Point{ComplexF64}
julia> Point{AbstractArray}
Main.__FRANKLIN_1273372.Point{AbstractArray}
julia> Point{Complex{Float64}} <: Point
true
julia> Point{AbstractArray} <: Point
true
julia> typeof(rand(10, 10)) <: Point
false
julia> # is equivalent to:
julia> rand(10, 10) isa Point
false
julia> isconcretetype(Point)
false
julia> isconcretetype(Point{AbstractArray})
true
What will happen when I run this code?
Point{Float16} <: Point{BigInt}
Float64 <: Real
Point{Float16} <: Point{Real}
A Point{Float16}
has a different representation from a Point{Real}
in memory which at a low level must contain two pointers to arbitrary subtypes of Real
while Point{Float16}
is just 4
-bytes
We can overcome this limitation if needed using the subtype operator <:
again:
julia> Point{Float16} <: Point{<:Real}
true
help?> AbstractArray
AbstractArray{T,N}
Supertype for N
-dimensional arrays (or array-like types) with elements of type T
. and other types are subtypes of this. See the manual section on the man-interface-array.
See also: , , , .
julia> typeof(Int64)
DataType
julia> typeof(Vector) # 🤔
UnionAll
julia> typeof(Vector{Float64})
DataType
The first and last examples right above were concrete types. But the middle one is a parametric type with no parameter!
When you think of a Tuple
type you should think of it as analogous to the arguments of a function:
function foo(a, b, c)
[a, b, c]' .^ 2 * [a, b, c]
end
foo (generic function with 1 method)