The simplest composite type is an immutable type. Instances of immutable types, like tuples, are values. Their fields cannot be changed after they are created. In many ways, an immutable type is like a Tuple
with names for the type itself and for each field.
Composite types, by definition, contain a number of simpler types. In Julia, this number can be zero; that is, an immutable type is allowed to contain no fields. This is comparable to the empty tuple ()
.
Why might this be useful? Such immutable types are known as "singleton types", as only one instance of them could ever exist. The values of such types are known as "singleton values". The standard library Base
contains many such singleton types. Here is a brief list:
Void
, the type of nothing
. We can verify that Void.instance
(which is special syntax for retrieving the singleton value of a singleton type) is indeed nothing
.MIME"text/plain"
, is a singleton type with a single instance, MIME("text/plain")
.Irrational{:π}
, Irrational{:e}
, Irrational{:φ}
, and similar types are singleton types, and their singleton instances are the irrational values π = 3.1415926535897...
, etc.Base.HasLength
, Base.HasShape
, Base.IsInfinite
, and Base.SizeUnknown
are all singleton types.sin
, for example, from typeof(sin).instance
.Because they contain nothing, singleton types are incredibly lightweight, and they can frequently be optimized away by the compiler to have no runtime overhead. Thus, they are perfect for traits, special tag values, and for things like functions that one would like to specialize on.
To define a singleton type,
julia> immutable MySingleton end
To define custom printing for the singleton type,
julia> Base.show(io::IO, ::MySingleton) = print(io, "sing")
To access the singleton instance,
julia> MySingleton.instance
MySingleton()
Often, one assigns this to a constant:
julia> const sing = MySingleton.instance
MySingleton()
If zero-field immutable types are interesting and useful, then perhaps one-field immutable types are even more useful. Such types are commonly called "wrapper types" because they wrap some underlying data, providing an alternative interface to said data. An example of a wrapper type in Base
is String
. We will define a similar type to String
, named MyString
. This type will be backed by a vector (one-dimensional array) of bytes (UInt8
).
First, the type definition itself and some customized showing:
immutable MyString <: AbstractString
data::Vector{UInt8}
end
function Base.show(io::IO, s::MyString)
print(io, "MyString: ")
write(io, s.data)
return
end
Now our MyString
type is ready for use! We can feed it some raw UTF-8 data, and it displays as we like it to:
julia> MyString([0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21])
MyString: Hello, World!
Obviously, this string type needs a lot of work before it becomes as usable as the Base.String
type.
Perhaps most commonly, many immutable types contain more than one field. An example is the standard library Rational{T}
type, which contains two fieds: a num
field for the numerator, and a den
field for the denominator. It is fairly straightforward to emulate this type design:
immutable MyRational{T}
num::T
den::T
MyRational(n, d) = (g = gcd(n, d); new(n÷g, d÷g))
end
MyRational{T}(n::T, d::T) = MyRational{T}(n, d)
We have successfully implemented a constructor that simplifies our rational numbers:
julia> MyRational(10, 6)
MyRational{Int64}(5,3)