Composite Types
Ringkasan Pelajaran
# Introduction
About
Composite Types
It is often useful to define custom types in our programs.
Creating new primitive types is possible, but rarely done. The built-in primitive types are not an arbitrary choice: they closely match the standard types in the LLVM compiler used by Julia for JIT compilation.
Much more useful is the the ability to define composite types, with named fields.
Other languages have something similar, calling them structs or records.
The Julia documentation refers to them as composite types, though (in a slight mismatch) the language syntax defines them with the struct keyword.
julia> struct Person
id
name::String
age::Integer
end
julia> fred = Person(23, "Fred Smith", 46)
Person(23, "Fred Smith", 46)
julia> typeof(fred)
Person
julia> isstructtype(Person)
true
julia> isconcretetype(Person)
true
julia> fred.age
46
A few points are worth noting in the above example:
- No type is specified for
id, so the compiler interprets this asid::Any.- Note:: in practice, it is best to help the compiler by specifying the type of a field with a concrete type.
- We will see in a future Concept that using parameters is another option.
- The type name is used as a constructor, as in
Person(id, name, age), to give an instance of typePerson. - Individual fields can be accessed with dot notation, the same as named tuples.
Items of type Person are immutable, so updates are not allowed.
julia> fred.age += 1
ERROR: setfield!: immutable struct of type Person cannot be changed
If mutability is necessary, add mutable to the definition.
julia> mutable struct MutPerson
name
age
end
julia> shaila = MutPerson("Shaila", 23)
MutPerson("Shaila", 23)
julia> shaila.age += 1
24
julia> shaila
MutPerson("Shaila", 24)
The constructor, by default, requires the fields to appear in the same order as the definition, which becomes inconvenient for more complicated types.
Using the @kwdef macro allows two new capabilities:
- The constructor uses keywords, not position.
- Fields can have default values, and optionally can be omitted from the constructor.
julia> @kwdef struct KwPerson
name
age = 42
end
KwPerson
julia> suki = KwPerson(age=29, name="Suki")
KwPerson("Suki", 29)
julia> qi = KwPerson(name="Qi")
KwPerson("Qi", 42)
# create a Vector{KwPerson}
julia> friends = [suki, qi]
2-element Vector{KwPerson}:
KwPerson("Suki", 29)
KwPerson("Qi", 42)
Composite Type Hierarchies
So far, we have only created concrete composite types.
If you check, all are subtypes of Any.
When creating multiple related types, it probably makes more sense to define a hierarchy for them. This is very easily done.
First, create an abstract type:
julia> abstract type AbstractPerson end
julia> supertype(AbstractPerson)
Any
Next, include a <: operator in a type definition, to show the relationship.
julia> struct ConcretePerson <: AbstractPerson
id
name::String
age::Integer
end
julia> supertype(ConcretePerson)
AbstractPerson
julia> ConcretePerson(15, "Luis", 8)
ConcretePerson(15, "Luis", 8)
Repeat as required, remembering that subtyping of concrete types is not allowed.
For a more complex hierarchy, create abstract subtypes as needed.
julia> abstract type AdultPerson <: AbstractPerson end
julia> supertype(AdultPerson)
AbstractPerson
The value of such a hierarchy will become clearer when we reach the Multiple Dispatch Concept, and see how functions handle argument types.
Inner Constructors
We saw at the beginning of this Concept that the name of the type is used as a constructor, to create items of that type:
julia> struct Person
id
name::String
age::Integer
end
julia> fred = Person(23, "Fred Smith", 46)
Person(23, "Fred Smith", 46)
Here, Person() is known as an outer constructor.
Field types will be checked for compatibility with the type definition, and an error raised if necessary.
Suppose we want constraints on the values passed in, not just the types?
Then we can include an inner constructor within the type definition, to carry out appropriate checks.
For example, we have an abstract type AbstractPoint, intended to take (x, y) coordinates, but want a subtype with the constraint y > x.
julia> abstract type AbstractPoint end;
julia> struct Point <: AbstractPoint
x::Integer
y::Integer
end;
julia> struct ConstrainedPoint <: AbstractPoint
x::Integer
y::Integer
ConstrainedPoint(x, y) =
y > x ? new(x, y) : @error("require y > x")
end;
julia> Point(3, 2)
Point(3, 2)
julia> ConstrainedPoint(3, 2)
Error: require y > x
julia> ConstrainedPoint(3, 5)
ConstrainedPoint(3, 5)
The new() function can only be used in inner constructors, and returns the desired item if the inputs are found to be valid.
For invalid input, we could stop with an error message, as in the example.
Alternatively, you may prefer to uses a placeholder such as nothing or missing.
See the Nothingness Concept for more on these.
Originally from Exercism julia concepts