Types and Methods

Types and functions

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.

In [1]:
three = 1 + 2
three::FloatingPoint
# typeof(three)
type: typeassert: expected FloatingPoint, got Int64
while loading In[1], in expression starting on line 2
In [2]:
super(String)
Out[2]:
Any
In [3]:
subtypes(String)
Out[3]:
8-element Array{Any,1}:
 DirectIndexString   
 GenericString       
 RepString           
 RevString{T<:String}
 RopeString          
 SubString{T<:String}
 UTF16String         
 UTF8String          

Parametrized types in Julia are invariant, not covariant or contravariant.

In [4]:
Array{Float64,1} <: Array{FloatingPoint,1}
Out[4]:
false

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:

In [5]:
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
Out[5]:
trapz (generic function with 1 method)
In [6]:
x = linspace(0, pi, 100);
y = sin(x);

println(trapz(x, y)); gc()
@time [trapz(x, y) for i in 1:1000];
1.9998321638939929
elapsed time: 0.000730233 seconds (47904 bytes allocated)

Methods and operators

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:

In [7]:
methods(+)  # 146 methods for generic function +
Out[7]:
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:

In [8]:
# +(s, t) = s * t

+(s::String, t::String) = s * t
Out[8]:
+ (generic function with 147 methods)
In [9]:
"abc" + " ... " + "xyz"
# +("abc", "...", "xyz")
Out[9]:
"abc ... xyz"

To define '++' as the operator for string concatenation is not possible (reason?); BUT:

In [10]:
(s::String, t::String) = s * t
# "abc" ⊕ " ... " ⊕ "xyz"
Out[10]:
⊕ (generic function with 1 method)

User-defined types

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.

In [11]:
immutable GaussInt <: Number  # or: type GaussInt
    a::Int
    b::Int

    # GaussInt(n::Int, m::Int) = new(n, m)
end
In [12]:
GaussInt(1,1)
Out[12]:
GaussInt(1,1)
In [13]:
import Base.show
show(io::IO, x::GaussInt) = show(io, complex(x.a, x.b))

GaussInt(1,1)
Out[13]:
1 + 1im
In [14]:
+(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);
In [15]:
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.