Videos related to this segment (click the title to watch)
01 JULIA1 - 5A: Types of types, composite types (17:23)
01 JULIA1 - 5B: Parametric types (7:37)
01 JULIA1 - 5C: Inheritance and composition OO paradigms (14:28)

0105 Custom Types

Some stuff to set-up the environment..

julia> cd(@__DIR__)
julia> using Pkg
julia> Pkg.activate(".") # If using a Julia version different than 1.10 please uncomment and run the following line (reproductibility guarantee will however be lost) # Pkg.resolve() # Pkg.instantiate() # run this if you didn't in Segment 01.01 Activating project at `~/work/SPMLJ/SPMLJ/buildedDoc/01_-_JULIA1_-_Basic_Julia_programming`
julia> using Random
julia> Random.seed!(123)Random.TaskLocalRNG()
julia> using InteractiveUtils # loaded automatically when working... interactively

"Type" of types

In Julia primitive, composite and abstract types can all be defined by the user

julia> primitive type APrimitiveType 819200 end # name and size in bit - multiple of 8 and below 8388608 (1MB)
julia> primitive type APrimitiveType2 819200 end
julia> 819200/(8*1024)100.0
julia> struct ACompositeType end # fields, constructors.. we'll see this later in details
julia> abstract type AnAbstractType end # no objects, no instantialisation of objects

Composite types

julia> mutable struct Foo # starting with a capital letter
           field1
           field2::String
           field3::ACompositeType
       end

mutable struct Foo # error! can't change a struct after I defined it end

julia> fieldnames(Foo)(:field1, :field2, :field3)
julia> o = Foo(123,"aaa",ACompositeType()) # call the default constructor (available automatically) - order matters!Main.var"Main".Foo(123, "aaa", Main.var"Main".ACompositeType())
julia> typeof(o)Main.var"Main".Foo

Outer constructor

julia> function Foo(f2,f3=ACompositeType()) # "normal" functions just happens it has the name of the object to create
           if startswith(f2,"Booo")         # put whatever logic you wish
               return nothing
           end
           return Foo(123,f2,f3)            # call the default constructor
       endMain.var"Main".Foo
julia> o = Foo(123,"aaa", ACompositeType()) # call the default constructorMain.var"Main".Foo(123, "aaa", Main.var"Main".ACompositeType())
julia> o = Foo("blaaaaa") # call the outer constructor we definedMain.var"Main".Foo(123, "blaaaaa", Main.var"Main".ACompositeType())
julia> o.field1 # access fields123
julia> o.field1 = 321 # modify field (because type defined as "mutable" !!!)321
julia> oMain.var"Main".Foo(321, "blaaaaa", Main.var"Main".ACompositeType())
julia> function Base.show(io::IO, x::Foo) # function(o) rather than o.function() println(io,"My custom representation for Foo objects") println(io,"Field1: $(o.field1)") println(io,"Field2: $(o.field2)") end
julia> oMy custom representation for Foo objects Field1: 321 Field2: blaaaaa

Inner constructor

julia> mutable struct Foo2
           field1::Int64
           field2::String
           function Foo2(f1,f2,f3)
               # ... logic
               return new(f1+f2,f3)
           end
       end
Tip

If any inner constructor method is defined, no default constructor method is provided.

julia> Foo2(1,2,"aaa")
       # Foo2(1,"aaa") # Error, no default constructor !Main.var"Main".Foo2(3, "aaa")

You can also use a macro (requires Julia 1.1) to automatically define an (outer) keyword_based constructor with support for optional arguments:

julia> Base.@kwdef struct Kfoo
          x::Int64 = 1
          y = 2
          z
       endMain.var"Main".Kfoo
julia> Kfoo(z=3)Main.var"Main".Kfoo(1, 2, 3)

Note that, at time of writing, @kwdef is not exported by Julia base, meaning that while widely used, it is considered still "experimental" and its usage may change in future Julia minor versions.

Custom pretty-printing

We can customise the way our custom type is rendered by overriding the Base.show function for our specific type.

We first need to import Base.show

julia> import Base.show
julia> mutable struct FooPoint x::Int64 y::Int64 end
julia> function show(io::IO, ::MIME"text/plain", x::FooPoint) # if get(io, :compact, true) ... # we can query the characteristics of the output print(io,"A FooPoint struct") endshow (generic function with 932 methods)
julia> function show(io::IO, x::FooPoint) # overridden by print print(io, "FooPoint x is $(x.x) and y is $(x.y) \nThat's all!") endshow (generic function with 933 methods)
julia> foo_obj=FooPoint(1,2)A FooPoint struct
julia> display(foo_obj)
julia> show(foo_obj)FooPoint x is 1 and y is 2 That's all!
julia> print(foo_obj)FooPoint x is 1 and y is 2 That's all!
julia> println(foo_obj)FooPoint x is 1 and y is 2 That's all!

Parametric types

julia> struct Point{T<:Number} # T must be a child of type "Number"
          x::T
          y::T
       end
julia> o = Point(1,2)Main.var"Main".Point{Int64}(1, 2)
julia> Point(1.0,2.) # Point(1,2.0) # error !Main.var"Main".Point{Float64}(1.0, 2.0)
julia> function Point(x::T, y::T=zero(T)) where {T} return Point(x,y) endMain.var"Main".Point
julia> Point(2)Main.var"Main".Point{Int64}(2, 0)
julia> Point(1.5)Main.var"Main".Point{Float64}(1.5, 0.0)
julia> abstract type Figure{T<:Number} end
julia> a = Array{Int64,2}(undef,2,2) # Array is nothing else than a parametric type with 2 parameters2×2 Matrix{Int64}: 112688 140147903425776 140147903425488 140147903426064
julia> typeof(a)Matrix{Int64} (alias for Array{Int64, 2})
julia> eltype(a)Int64

As we see for arrays, parameters doesn't need to be types, but can be any value of a bits type (in practice an integer value) :

julia> struct MyType{T,N}
         data::Array{T,N}
       end
julia> intMatrixInside = MyType([1 2 3; 4 5 6])Main.var"Main".MyType{Int64, 2}([1 2 3; 4 5 6])
julia> floatVectorInside = MyType([1 2 3])Main.var"Main".MyType{Int64, 2}([1 2 3])
julia> function getPlane(o::MyType{T,N},dim,pos) where {T,N} sizes = size(o.data) if length(sizes) > N error("Dim over the dimensions of the data") elseif sizes[dim] < pos error("Non enought elements in dimension $dim to cut at $pos") end return selectdim(o.data,dim,pos) endgetPlane (generic function with 1 method)
julia> getPlane(intMatrixInside,1,2)3-element view(::Matrix{Int64}, 2, :) with eltype Int64: 4 5 6

A package where non-type parameters are emploied to boost speed is StaticArray.jl where one parameter is the size of the array that hence become known at compile time

Inheritance

julia> abstract type MyOwnGenericAbstractType end                       # the highest-level
julia> abstract type MyOwnAbstractType1 <: MyOwnGenericAbstractType end # child of MyOwnGenericAbstractType
julia> abstract type MyOwnAbstractType2 <: MyOwnGenericAbstractType end # also child of MyOwnGenericAbstractType
julia> mutable struct AConcreteTypeA <: MyOwnAbstractType1 f1::Int64 f2::Int64 end
julia> mutable struct AConcreteTypeB <: MyOwnAbstractType1 f1::Float64 end
julia> mutable struct AConcreteTypeZ <: MyOwnAbstractType2 f1::String end
julia> oA = AConcreteTypeA(2,10)Main.var"Main".AConcreteTypeA(2, 10)
julia> oB = AConcreteTypeB(1.5)Main.var"Main".AConcreteTypeB(1.5)
julia> oZ = AConcreteTypeZ("aa")Main.var"Main".AConcreteTypeZ("aa")
julia> supertype(AConcreteTypeA)Main.var"Main".MyOwnAbstractType1
julia> subtypes(MyOwnAbstractType1)Any[]
Tip

When multiple methods are available for an object, function calls are dispatched to the most stricter method, i.e. the one defined over the exact parameter's type or their immediate parents

julia> function foo(a :: MyOwnGenericAbstractType)                      # good for everyone
         println("Default implementation: $(a.f1)")
       endfoo (generic function with 1 method)
julia> foo(oA) # Default implementation: 2Default implementation: 2
julia> foo(oB) # Default implementation: 1.5Default implementation: 1.5
julia> foo(oZ) # Default implementation: aaDefault implementation: aa
julia> function foo(a :: MyOwnAbstractType1) # specialisation for MyOwnAbstractType1 println("A more specialised implementation: $(a.f1*4)") endfoo (generic function with 2 methods)
julia> foo(oA) # A more specialised implementation: 8A more specialised implementation: 8
julia> foo(oB) # A more specialised implementation: 6.0A more specialised implementation: 6.0
julia> foo(oZ) # Default implementation: aa # doesn't match the specialisation, default to foo(a :: MyOwnGenericAbstractType)Default implementation: aa
julia> function foo(a :: AConcreteTypeA) println("A even more specialised implementation: $(a.f1 + a.f2)") endfoo (generic function with 3 methods)
julia> foo(oA) # A even more specialised implementation: 12A even more specialised implementation: 12
julia> foo(oB) # A more specialised implementation: 6.0A more specialised implementation: 6.0
julia> foo(oZ) # Default implementation: aaDefault implementation: aa
Warning

Attention to the inheritance for parametric types. If it is true that Vector{Int64} <: AbstractVector{Int64} and Int64 <: Number, it is FALSE that AbstractVector{Int64} <: AbstractVector{Number}. If you want to allow a function parameter to be a vector of numbers, use instead templates explicitly, e.g. foo(x::AbstractVector{T}) where {T<:Number} = return sum(x)

julia> Vector{Int64} <: AbstractVector{Int64}true
julia> Int64 <: Numbertrue
julia> Vector{Int64} <: Vector{Number}false
julia> AbstractVector{Int64} <: AbstractVector{Number}false

Object-oriented model

OO model based on composition

julia> struct Shoes
          shoesType::String
          colour::String
       end
julia> struct Person myname::String age::Int64 end
julia> struct Student p::Person # by referencing a `Person`` object, we do not need to repeat its fields school::String shoes::Shoes # same for `shoes` end
julia> struct Employee p::Person monthlyIncomes::Float64 company::String shoes::Shoes end
julia> gymShoes = Shoes("gym","white")Main.var"Main".Shoes("gym", "white")
julia> proShoes = Shoes("classical","brown")Main.var"Main".Shoes("classical", "brown")
julia> Marc = Student(Person("Marc",15),"Divine School",gymShoes)Main.var"Main".Student(Main.var"Main".Person("Marc", 15), "Divine School", Main.var"Main".Shoes("gym", "white"))
julia> MrBrown = Employee(Person("Brown",45),3200.0,"ABC Corporation Inc.", proShoes)Main.var"Main".Employee(Main.var"Main".Person("Brown", 45), 3200.0, "ABC Corporation Inc.", Main.var"Main".Shoes("classical", "brown"))
julia> function printMyActivity(self::Student) println("Hi! I am $(self.p.myname), I study at $(self.school) school, and I wear $(self.shoes.colour) shoes") # I can use the dot operator chained... endprintMyActivity (generic function with 1 method)
julia> function printMyActivity(self::Employee) println("Good day. My name is $(self.p.myname), I work at $(self.company) company and I wear $(self.shoes.colour) shoes") endprintMyActivity (generic function with 2 methods)
julia> printMyActivity(Marc) # Hi! I am Marc, ...Hi! I am Marc, I study at Divine School school, and I wear white shoes
julia> printMyActivity(MrBrown) # Good day. My name is MrBrown, ...Good day. My name is Brown, I work at ABC Corporation Inc. company and I wear brown shoes

OO models based on Specialisation (Person → Student) or Weack Relation (Person → Shoes) instead of Composition (Person → Arm) can be implemented using third party packages, like e.g. SimpleTraits.jl or OOPMacro.jl

View this file on Github.


This page was generated using Literate.jl.