One of the most important goals of high-level languages is to provide polymorphism: the ability for the same code to operate on different kinds of values.
Julia uses a vocabulary of types for this purpose. Types play the following roles:
typeof(3)
sizeof(Int64)
Int64.size
isbits(Int64)
Int64.mutable
supertype(Int64)
supertype(Signed)
supertype(Integer)
supertype(Real)
supertype(Number)
# The subtype operator/relation
Integer <: Real
String <: Real
Any >: String
# The `isa` operator/relation
1 isa Int
1 isa String
Julia has roughly 5 kinds of types. We just saw two:
There are three more:
Expresses a set union of types.
1 isa Union{Int,String}
"hi" isa Union{Int,String}
Expresses an iterated set union of types.
[1] isa Vector{Int}
[1] isa (Vector{T} where T<:Real)
$\bigcup\limits_{T<:Real} \tt{Vector}\{T\}$
Union{Vector{Any},Vector{Real}} <: Vector{T} where T>:Real
T where T<:Real
rand(1:10,2,2)
dump(Array)
Vector
Vector{Int} <: Vector
Vector <: Array
Vector{Int} <: Vector{Any}
typeintersect((Array{T} where T<:Real), (Array{T,2} where T>:Int))
[[2]] isa (Vector{T} where T<:Vector{S} where S<:Integer)
Corresponds to the empty set.
1 isa Union{}
Union{} <: Int
Union{} <: String
Union{} <: Array
This represents situations where there can't be any value; e.g. an exception is thrown or the program doesn't terminate.
f(a, b::Any) = "fallback"
f(a::Number, b::Number) = "a and b are both numbers"
f(a::Number, b) = "a is a number"
f(a, b::Number) = "b is a number"
f(a::Integer, b::Integer) = "a and b are both integers"
methods(f)
f(1.5, 2)
f(1, "string")
f(1, 2)
f(1, 2, 3)
A tuple is an immutable container of any combination of values.
Often used to represent e.g. ordered pairs, or for returning "multiple" values from functions.
t = (1, "hi", 0.33, pi)
t[2]
# "destructuring"
a, b, c = t
a
b
typeof(t)
Tuple types represent the arguments to a function.
first(methods(f)).sig
For every function call, the method that gets called is the most specific one such that the argument tuple type is a subtype of the signature.
d(x::T, y::T) where {T} = "same type"
d(x, y) = "different types"
d(1, 1)
d(1, 2.0)
[ m.sig for m in methods(d) ]
v(x...) = (x, "zero or more")
v(x, xs...) = (xs, "one or more")
v()
v(1)
v(1, 2, 3, 4)
foo(a::Array, Is::Int...) = 0
first(methods(foo)).sig
vt = Tuple{Array, Vararg{Int}}
isa(([1],1,2,3), vt)
isa(([1],1,0.02,3), vt)
Internally, the compiler generates specializations for particular types.
Example: For a 3-argument function f
, the compiler might decide to generate a specialization for Tuple{Int, Any, Int}
, if for some reason the second argument isn't important.
addall(t) = +(t...) # "splat"
@code_typed addall((1,2))
@code_typed addall((1,2,3))
function alltrue(f, itr)
@inbounds for x in itr
f(x) || return false
end
return true
end
@which isinteger(1)
@code_typed alltrue(isinteger, [1,2,3])
@code_llvm alltrue(isinteger, [1,2,3])
Dynamic dispatch is traditionally considered "slow".
Instead of a call
instruction, you need to do a table lookup procedure first.
However:
We can't specialize on everything because it would take too long and generate too much code.
There's no fully general and automatic approach.
We specialize on types. That's a reasonable default. If the default's not good enough, move more information into types!
A classic: specializing on the value of an integer.
function sum1n(::Val{N}) where {N} # given `struct Val{N} end`
s = 0
for i = 1:N
s += i
end
return s
end
sum1n(Val{10}())
@code_llvm sum1n(Val{10}())
sum1n(n::Integer) = sum1n(Val{n}())
sum1n(20)
sum1n(rand(1:100)) # dynamic dispatch to specialized code
The compiler's optimizations can be exploited to move parts of your own computations to compile time (thus saving time at run time). The general idea is to represent more information within types, instead of using values.
Example: drop the first element of a tuple.
tuple_tail1(t) = t[2:end]
tuple_tail1((1,2,"hi"))
@code_typed tuple_tail1((1,2,"hi"))
Not good. Key information is represented as integers, and when the compiler sees an integer it generally assumes it doesn't know its value.
argtail(a, rest...) = rest
tupletail(t) = argtail(t...)
tupletail((1,2,"hi"))
@code_typed tupletail((1,2,"hi"))
Write a type-inferable function to...
index_shape(a::Array, idxs) = ish(a, 1, idxs...)
ish(a, i, ::Real...) = ()
ish(a, i, ::Colon, rest...) = (size(a,i), ish(a,i+1,rest...)...)
ish(a, i, iv::Vector, rest...) = (length(iv), ish(a,i+1,rest...)...)
ish(a, i, ::Real, rest...) = ish(a,i+1,rest...)
index_shape(rand(3,4,5), (1,:,[1,2]))
index_shape(rand(3,4,5), (:,2,[1,2,1,2,1,2,1,2]))
widen(::Type{Float32}) = Float64
widen(Float32)
# We use this for type promotion
promote_type(Int64, Float64)
This can be used to compute attributes of types, then dispatch on those values.
# Sample trait
abstract IteratorSize
immutable SizeUnknown <: IteratorSize end
immutable HasLength <: IteratorSize end
immutable HasShape <: IteratorSize end
immutable IsInfinite <: IteratorSize end
Now we can define a method that says which value of the trait a certain type has.
This is like using dispatch as a lookup table to find out properties of a combination of values.
iteratorsize{T<:AbstractArray}(::Type{T}) = HasShape()
iteratorsize{I1,I2}(::Type{Zip2{I1,I2}}) = zip_iteratorsize(iteratorsize(I1),iteratorsize(I2))
zip_iteratorsize(a, b) = SizeUnknown()
zip_iteratorsize{T}(isz::T, ::T) = isz
zip_iteratorsize(::HasLength, ::HasShape) = HasLength()
zip_iteratorsize(::HasShape, ::HasLength) = HasLength()
# `collect` gives you all the elements from an iterator as an array
collec(itr) = _collec(itr, eltype(itr), Base.iteratorsize(itr))
function _collec(itr, T, ::Base.HasLength)
a = Array{T,1}(length(itr))
i = 0
for x in itr
a[i+=1] = x
end
return a
end
function _collec(itr, T, ::Base.SizeUnknown)
a = Array{T,1}(0)
for x in itr
push!(a, x)
end
return a
end