Julia's type system is dynamic, nominativ/declarative, and (invariantly) parametric.
Concrete types have a fixed prepresentation in computer storage, objects of abstract types may have quite different representations. For instance, a matrix of type Array{Any,2}
needs to use pointers to locations in memory, but if of type Array{Int64,2}
it can use indices into a contiguous memory section. This makes matrices of concrete types much more efficient.
three = 1 + 2
three::FloatingPoint
# typeof(three)
super(String)
subtypes(String)
Parametrized types in Julia are invariant, not covariant or contravariant.
Array{Float64,1} <: Array{FloatingPoint,1}
There is no loss in performance if the programmer relies on duck typing or on functions whose arguments are abstract types, because the function is recompiled for each tuple of argument of concrete types with which it is invoked.
Providing typed parameters for a functions, i.e. defining the signature, helps with error handling and type stability, and makes method dispatch possible.
This is how our trapz
function could look like with typed parameters:
function trapz{T<:Number}(x::Array{T,1}, y::Array{T,1})
local n = length(x)
if (length(y) != n)
error("Vectors 'x', 'y' must be of same length")
end
r = zero(T)
if n <= 1 return r end
for i in 2:n
@inbounds r += (x[i] - x[i-1]) * (y[i] + y[i-1])
end
r / (one(T) + one(T))
end
x = linspace(0, pi, 100);
y = sin(x);
println(trapz(x, y)); gc()
@time [trapz(x, y) for i in 1:1000];
Functions quite often will not be defined all at once, but can rather be defined step by step, providing specific behaviors for certain combinations of argument types and counts. A definition of one possible behavior for a function is called a method. Methods of the same name and belonging to one function will differ by signature and implementation.
Example: The '+
' function (or operator ) is actually a set of 125 different methods:
methods(+) # 146 methods for generic function +
Operators, unary or binary, are functions/methods, too, and can be overloaded. These operators can be applied in prefix or infix form.
Example continued: Define '+
' as the operator for concatenating strings:
# +(s, t) = s * t
+(s::String, t::String) = s * t
"abc" + " ... " + "xyz"
# +("abc", "...", "xyz")
To define '++
' as the operator for string concatenation is not possible (reason?); BUT:
⊕(s::String, t::String) = s * t
# "abc" ⊕ " ... " ⊕ "xyz"
The user can define his own types, most of the time composite types resembling records, structures, or objects in other languages. The following example is meant to only demonstrate the basic syntax.
Define the type of "Gaussian integers", that are numbers \(n + m \sqrt{-1}\) where \(n, m\) are whole numbers.
immutable GaussInt <: Number # or: type GaussInt
a::Int
b::Int
# GaussInt(n::Int, m::Int) = new(n, m)
end
GaussInt(1,1)
import Base.show
show(io::IO, x::GaussInt) = show(io, complex(x.a, x.b))
GaussInt(1,1)
+(x::GaussInt, y::GaussInt) = GaussInt(x.a + y.a, x.b + y.b);
-(x::GaussInt, y::GaussInt) = GaussInt(x.a - y.a, x.b - y.b);
*(x::GaussInt, y::GaussInt) = GaussInt(x.a*y.a - x.b*y.b, x.a*y.b + x.b*y.a);
import Base.norm, Base.isprime
norm(x::GaussInt) = x.a^2 + x.b^2;
isprime(x::GaussInt) = isprime(norm(x)); # wrong
function isunit(x::GaussInt) norm(x) == 1 || norm(x) == -1 ? true : false end;
For a more complete definition of Gaussian integers as a Julia module, see file GaussInt.jl
.