The Magic of Julia Data Types¶

(Back to Overview)

Unlike object-oriented languaes (eg. C++ and Python), where classes own methods, in Julia there are no classes in the onject-oriented sense:

  • Data types define structured data and nothing else (mostly)
  • Functions can have multiple definitions (each new definition of a function is called a method)
  • The Julia compiler chooses which method to apply to a function call based on the type of all input arguments (i.e. Multiple Dispatch)

This might be a bit strange for python users: classes look "inside-out" with method definitions accompanying data types.

For encapsulation, use Modules!

Composite Data Types (Classes)¶

Let's define an ordered pair of reals

In [1]:
struct OrderedPair <: Number
    x::Real
    y::Real
    
    OrderedPair(x::Real, y::Real) = x > y ? error("out of order") : new(x,y)
end

The encapsulated function: OrderedPair(x,y) = x > y ? error("out of order") : new(x,y) defines a constructor that ensure that the ordered pair stays ordered. In Python, this would be handled by __init__(self, x, y)

The is a one-line function definition here is equivalent to:

function OrderedPair(x,y)
    if x > y
        error("out of order")
    end
    new(x,y)
end

Let's define some math: addition and subtraction:

  • In Python we would define __add__ and __sub__
  • In Julia we define new methods for Base.+ and Base.-

We also need conversion and promotion rules for our custom data type. This way our OrderedPair is a first-class citizen ... just like Float64 :)

In [2]:
import Base: +, -, convert, promote_rule

function +(a::OrderedPair, b::OrderedPair)
    x_new = a.x + b.x
    y_new = a.y + b.y
    OrderedPair(x_new, y_new)
end

# One-liners can seem a bit magical
-(a::OrderedPair, b::OrderedPair) = OrderedPair(a.x - b.x, a.y - b.y)

# Getting these right might require some experimenting. Note how OrderedPair extends Number.
convert(::Type{OrderedPair}, x::Real) = OrderedPair(x, x)
promote_rule(::Type{OrderedPair}, ::Type{<:Real}) = OrderedPair
Out[2]:
promote_rule (generic function with 125 methods)

We can now use our order pairs in addition and subtraction:

In [3]:
p1 = OrderedPair(1, 2)
p2 = OrderedPair(10, 20)
Out[3]:
OrderedPair(10, 20)
In [4]:
p1 + p2
Out[4]:
OrderedPair(11, 22)

Our constructor ensures that the ordered pair type remains consistent (ie. ordered)

In [5]:
p1 - p2
out of order

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:33
 [2] OrderedPair(x::Int64, y::Int64)
   @ Main ./In[1]:5
 [3] -(a::OrderedPair, b::OrderedPair)
   @ Main ./In[2]:10
 [4] top-level scope
   @ In[5]:1
 [5] eval
   @ ./boot.jl:360 [inlined]
 [6] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
   @ Base ./loading.jl:1094
In [6]:
p2 - p1
Out[6]:
OrderedPair(9, 18)

What are conversion and promotion used for? Let's say we want to convert the number 20 to an ordered pair -- that's where conversion is used:

In [7]:
convert(OrderedPair, 20.)
Out[7]:
OrderedPair(20.0, 20.0)

Promotion defines what type the result of two different inputs' data types should have. Together with conversion we can add a single number to both parts of the pair:

In [8]:
p1 + 20.
Out[8]:
OrderedPair(21.0, 22.0)

Generics and UnionAll Data Types¶

We can use curly braces and the where keyword to define generic types. You can think of these as basically C++ templates. The <: symbol restricts the possible inputs to the generic type T

In [9]:
struct OrderedPair2{T} <: Number where T <: Number
    x::T
    y::T
    OrderedPair2(x::T, y::T) where T = x > y ? error("out of order") : new{T}(x,y)
end

This now generates different specializations based on the type of x, and y

In [10]:
p = OrderedPair2(1., 2.)
Out[10]:
OrderedPair2{Float64}(1.0, 2.0)
In [11]:
p2 = OrderedPair2(1, 2)
Out[11]:
OrderedPair2{Int64}(1, 2)