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 end
Main.var"Main".Foo
julia> o = Foo(123,"aaa", ACompositeType()) # call the default constructor
Main.var"Main".Foo(123, "aaa", Main.var"Main".ACompositeType())
julia> o = Foo("blaaaaa") # call the outer constructor we defined
Main.var"Main".Foo(123, "blaaaaa", Main.var"Main".ACompositeType())
julia> o.field1 # access fields
123
julia> o.field1 = 321 # modify field (because type defined as "mutable" !!!)
321
julia> o
Main.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> o
My 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
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 end
Main.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") end
show (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!") end
show (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) end
Main.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 parameters
2×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) end
getPlane (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[]
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)") end
foo (generic function with 1 method)
julia> foo(oA) # Default implementation: 2
Default implementation: 2
julia> foo(oB) # Default implementation: 1.5
Default implementation: 1.5
julia> foo(oZ) # Default implementation: aa
Default implementation: aa
julia> function foo(a :: MyOwnAbstractType1) # specialisation for MyOwnAbstractType1 println("A more specialised implementation: $(a.f1*4)") end
foo (generic function with 2 methods)
julia> foo(oA) # A more specialised implementation: 8
A more specialised implementation: 8
julia> foo(oB) # A more specialised implementation: 6.0
A 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)") end
foo (generic function with 3 methods)
julia> foo(oA) # A even more specialised implementation: 12
A even more specialised implementation: 12
julia> foo(oB) # A more specialised implementation: 6.0
A more specialised implementation: 6.0
julia> foo(oZ) # Default implementation: aa
Default implementation: aa
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 <: Number
true
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... end
printMyActivity (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") end
printMyActivity (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
This page was generated using Literate.jl.