Julia Notes Day3 -- Composite and Parametric Types

·

3 min read

Composite types are called records, structs, or objects in various languages.

struct Foo
    bar
    baz::Int
    qux::Float64
end

foo = Foo("Hello, world.", 23, 1.5) # Foo("Hello, world.", 23, 1.5)

foo.bar # "Hello, world."

Composite objects declared with struct are immutable; they cannot be modified after construction.

An immutable object might contain mutable objects, such as arrays, as fields. Those contained objects will remain mutable; only the fields of the immutable object itself cannot be changed to point to different objects.

Where required, mutable composite objects can be declared with the keyword mutable struct.

In cases where one or more fields of an otherwise mutable struct is known to be immutable, one can declare these fields as such using const as shown below.

mutable struct Baz
    a::Int
    const b::Float64
end

baz = Baz(1, 1.5) # Baz(1, 1.5)
baz.a = 3;
baz # Baz(3, 1.5)

An important and powerful feature of Julia's type system is that it is parametric: types can take parameters, so that type declarations actually introduce a whole family of new types – one for each possible combination of parameter values.

struct Point{T<:Real}
    x::T
    y::T
end

Without any explicitly provided inner constructors, the declaration of the composite type Point{T<:Real} automatically provides an inner constructor, Point{T}, for each possible type T<:Real, that behaves just like non-parametric default inner constructors do. It also provides a single general outer Point constructor that takes pairs of real arguments, which must be of the same type. This automatic provision of constructors is equivalent to the following explicit declaration:

struct Point{T<:Real}
   x::T
   y::T
   Point{T}(x,y) where {T<:Real} = new(x,y)
end

Point(x::T, y::T) where {T<:Real} = Point{T}(x,y);

By default, instances of parametric composite types can be constructed either with explicitly given type parameters or with type parameters implied by the types of the arguments given to the constructor. Here are some examples:

p1 = Point(7,5) ## implicit T ## Point{Int64}(7,5)
p2 = Point(2,3) ## implicit T ## Point{Int64}(2,3)

Point{Float64}(1,2) ## explicit T ## Point{Float64}(1.0, 2.0)

p1 = Point(5,1.5) 
# ERROR: MethodError: no method matching Point(::Int64, ::Float64)

For a more general way to make all such calls work sensibly, all it takes is the following outer method definition to make all calls to the general Point constructor work as one would expect:

Point(x::Real, y::Real) = Point(promote(x,y)...);

# Point(Float64, Int) -> Point(Float64, Float64)
Point(1.5,2) # Point{Float64}(1.5, 2.0)

The promote function converts all its arguments to a common type – in this case Float64. With this method definition, the Point constructor promotes its arguments the same way that numeric operators like + do, and works for all kinds of real numbers.

In mainstream object oriented languages, such as C++, Java, Python and Ruby, composite types also have named functions associated with them, and the combination is called an "object".

In Julia, all values are objects, but functions are always not bundled with the objects they operate on.This is necessary since Julia chooses which method of a function to use by multiple dispatch, meaning that the types of all of a function's arguments are considered when selecting a method, rather than just the first one.

Base.show(io::IO, z::Point) = print(io, "{\n  x=$(z.x)\n  y=$(z.y)\n}")

Point(2.3, 5)
#= 
{
  x=2.3
  y=5.0
}
=#