Type systems have traditionally fallen into two quite different camps: static type systems, where every program expression must have a type computable before the execution of the program, and dynamic type systems, where nothing is known about types until run time, when the actual values manipulated by the program are available. Object orientation allows some flexibility in statically typed languages by letting code be written without the precise types of values being known at compile time. The ability to write code that can operate on different types is called polymorphism. All code in classic dynamically typed languages is polymorphic: only by explicitly checking types, or when objects fail to support operations at run-time, are the types of any values ever restricted.
Julia's type system is dynamic, but gains some of the advantages of static type systems by making it possible to indicate that certain values are of specific types. This can be of great assistance in generating efficient code, but even more significantly, it allows method dispatch on the types of function arguments to be deeply integrated with the language. Method dispatch is explored in detail inMethods, but is rooted in the type system presented here.
The default behavior in Julia when types are omitted is to allow values to be of any type. Thus, one can write many useful Julia functions without ever explicitly using types. When additional expressiveness is needed, however, it is easy to gradually introduce explicit type annotations into previously "untyped" code. Adding annotations serves three primary purposes: to take advantage of Julia's powerful multiple-dispatch mechanism, to improve human readability, and to catch programmer errors.
Describing Julia in the lingo oftype systems, it is: dynamic, nominative and parametric. Generic types can be parameterized, and the hierarchical relationships between types areexplicitly declared, rather thanimplied by compatible structure. One particularly distinctive feature of Julia's type system is that concrete types may not subtype each other: all concrete types are final and may only have abstract types as their supertypes. While this might at first seem unduly restrictive, it has many beneficial consequences with surprisingly few drawbacks. It turns out that being able to inherit behavior is much more important than being able to inherit structure, and inheriting both causes significant difficulties in traditional object-oriented languages. Other high-level aspects of Julia's type system that should be mentioned up front are:
isbits
returns true (essentially, things like numbers and bools that are stored like C types orstruct
s with no pointers to other objects), and also by tuples thereof. Type parameters may be omitted when they do not need to be referenced or restricted.Julia's type system is designed to be powerful and expressive, yet clear, intuitive and unobtrusive. Many Julia programmers may never feel the need to write code that explicitly uses types. Some kinds of programming, however, become clearer, simpler, faster and more robust with declared types.
The::
operator can be used to attach type annotations to expressions and variables in programs. There are two primary reasons to do this:
When appended to an expression computing a value, the::
operator is read as "is an instance of". It can be used anywhere to assert that the value of the expression on the left is an instance of the type on the right. When the type on the right is concrete, the value on the left must have that type as its implementation – recall that all concrete types are final, so no implementation is a subtype of any other. When the type is abstract, it suffices for the value to be implemented by a concrete type that is a subtype of the abstract type. If the type assertion is not true, an exception is thrown, otherwise, the left-hand value is returned:
julia> (1+2)::AbstractFloatERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64julia> (1+2)::Int3
This allows a type assertion to be attached to any expression in-place.
When appended to a variable on the left-hand side of an assignment, or as part of alocal
declaration, the::
operator means something a bit different: it declares the variable to always have the specified type, like a type declaration in a statically-typed language such as C. Every value assigned to the variable will be converted to the declared type usingconvert
:
julia> function foo() x::Int8 = 100 x endfoo (generic function with 1 method)julia> x = foo()100julia> typeof(x)Int8
This feature is useful for avoiding performance "gotchas" that could occur if one of the assignments to a variable changed its type unexpectedly.
This "declaration" behavior only occurs in specific contexts:
local x::Int8 # in a local declarationx::Int8 = 10 # as the left-hand side of an assignment
and applies to the whole current scope, even before the declaration.
As of Julia 1.8, type declarations can now be used in global scope i.e. type annotations can be added to global variables to make accessing them type stable.
julia> x::Int = 1010julia> x = 3.5ERROR: InexactError: Int64(3.5)julia> function foo(y) global x = 15.8 # throws an error when foo is called return x + y endfoo (generic function with 1 method)julia> foo(10)ERROR: InexactError: Int64(15.8)
Declarations can also be attached to function definitions:
function sinc(x)::Float64 if x == 0 return 1 end return sin(pi*x)/(pi*x)end
Returning from this function behaves just like an assignment to a variable with a declared type: the value is always converted toFloat64
.
Abstract types cannot be instantiated, and serve only as nodes in the type graph, thereby describing sets of related concrete types: those concrete types which are their descendants. We begin with abstract types even though they have no instantiation because they are the backbone of the type system: they form the conceptual hierarchy which makes Julia's type system more than just a collection of object implementations.
Recall that inIntegers and Floating-Point Numbers, we introduced a variety of concrete types of numeric values:Int8
,UInt8
,Int16
,UInt16
,Int32
,UInt32
,Int64
,UInt64
,Int128
,UInt128
,Float16
,Float32
, andFloat64
. Although they have different representation sizes,Int8
,Int16
,Int32
,Int64
andInt128
all have in common that they are signed integer types. LikewiseUInt8
,UInt16
,UInt32
,UInt64
andUInt128
are all unsigned integer types, whileFloat16
,Float32
andFloat64
are distinct in being floating-point types rather than integers. It is common for a piece of code to make sense, for example, only if its arguments are some kind of integer, but not really depend on what particularkind of integer. For example, the greatest common denominator algorithm works for all kinds of integers, but will not work for floating-point numbers. Abstract types allow the construction of a hierarchy of types, providing a context into which concrete types can fit. This allows you, for example, to easily program to any type that is an integer, without restricting an algorithm to a specific type of integer.
Abstract types are declared using theabstract type
keyword. The general syntaxes for declaring an abstract type are:
abstract type «name» endabstract type «name» <: «supertype» end
Theabstract type
keyword introduces a new abstract type, whose name is given by«name»
. This name can be optionally followed by<:
and an already-existing type, indicating that the newly declared abstract type is a subtype of this "parent" type.
When no supertype is given, the default supertype isAny
– a predefined abstract type that all objects are instances of and all types are subtypes of. In type theory,Any
is commonly called "top" because it is at the apex of the type graph. Julia also has a predefined abstract "bottom" type, at the nadir of the type graph, which is written asUnion{}
. It is the exact opposite ofAny
: no object is an instance ofUnion{}
and all types are supertypes ofUnion{}
.
Let's consider some of the abstract types that make up Julia's numerical hierarchy:
abstract type Number endabstract type Real <: Number endabstract type AbstractFloat <: Real endabstract type Integer <: Real endabstract type Signed <: Integer endabstract type Unsigned <: Integer end
TheNumber
type is a direct child type ofAny
, andReal
is its child. In turn,Real
has two children (it has more, but only two are shown here; we'll get to the others later):Integer
andAbstractFloat
, separating the world into representations of integers and representations of real numbers. Representations of real numbers include floating-point types, but also include other types, such as rationals.AbstractFloat
includes only floating-point representations of real numbers. Integers are further subdivided intoSigned
andUnsigned
varieties.
The<:
operator in general means "is a subtype of", and, used in declarations like those above, declares the right-hand type to be an immediate supertype of the newly declared type. It can also be used in expressions as a subtype operator which returnstrue
when its left operand is a subtype of its right operand:
julia> Integer <: Numbertruejulia> Integer <: AbstractFloatfalse
An important use of abstract types is to provide default implementations for concrete types. To give a simple example, consider:
function myplus(x,y) x+yend
The first thing to note is that the above argument declarations are equivalent tox::Any
andy::Any
. When this function is invoked, say asmyplus(2,5)
, the dispatcher chooses the most specific method namedmyplus
that matches the given arguments. (SeeMethods for more information on multiple dispatch.)
Assuming no method more specific than the above is found, Julia next internally defines and compiles a method calledmyplus
specifically for twoInt
arguments based on the generic function given above, i.e., it implicitly defines and compiles:
function myplus(x::Int,y::Int) x+yend
and finally, it invokes this specific method.
Thus, abstract types allow programmers to write generic functions that can later be used as the default method by many combinations of concrete types. Thanks to multiple dispatch, the programmer has full control over whether the default or more specific method is used.
An important point to note is that there is no loss in performance if the programmer relies on a function whose arguments are abstract types, because it is recompiled for each tuple of concrete argument types with which it is invoked. (There may be a performance issue, however, in the case of function arguments that are containers of abstract types; seePerformance Tips.)
It is almost always preferable to wrap an existing primitive type in a new composite type than to define your own primitive type.
This functionality exists to allow Julia to bootstrap the standard primitive types that LLVM supports. Once they are defined, there is very little reason to define more.
A primitive type is a concrete type whose data consists of plain old bits. Classic examples of primitive types are integers and floating-point values. Unlike most languages, Julia lets you declare your own primitive types, rather than providing only a fixed set of built-in ones. In fact, the standard primitive types are all defined in the language itself:
primitive type Float16 <: AbstractFloat 16 endprimitive type Float32 <: AbstractFloat 32 endprimitive type Float64 <: AbstractFloat 64 endprimitive type Bool <: Integer 8 endprimitive type Char <: AbstractChar 32 endprimitive type Int8 <: Signed 8 endprimitive type UInt8 <: Unsigned 8 endprimitive type Int16 <: Signed 16 endprimitive type UInt16 <: Unsigned 16 endprimitive type Int32 <: Signed 32 endprimitive type UInt32 <: Unsigned 32 endprimitive type Int64 <: Signed 64 endprimitive type UInt64 <: Unsigned 64 endprimitive type Int128 <: Signed 128 endprimitive type UInt128 <: Unsigned 128 end
The general syntaxes for declaring a primitive type are:
primitive type «name» «bits» endprimitive type «name» <: «supertype» «bits» end
The number of bits indicates how much storage the type requires and the name gives the new type a name. A primitive type can optionally be declared to be a subtype of some supertype. If a supertype is omitted, then the type defaults to havingAny
as its immediate supertype. The declaration ofBool
above therefore means that a boolean value takes eight bits to store, and hasInteger
as its immediate supertype. Currently, only sizes that are multiples of 8 bits are supported and you are likely to experience LLVM bugs with sizes other than those used above. Therefore, boolean values, although they really need just a single bit, cannot be declared to be any smaller than eight bits.
The typesBool
,Int8
andUInt8
all have identical representations: they are eight-bit chunks of memory. Since Julia's type system is nominative, however, they are not interchangeable despite having identical structure. A fundamental difference between them is that they have different supertypes:Bool
's direct supertype isInteger
,Int8
's isSigned
, andUInt8
's isUnsigned
. All other differences betweenBool
,Int8
, andUInt8
are matters of behavior – the way functions are defined to act when given objects of these types as arguments. This is why a nominative type system is necessary: if structure determined type, which in turn dictates behavior, then it would be impossible to makeBool
behave any differently thanInt8
orUInt8
.
Composite types are called records, structs, or objects in various languages. A composite type is a collection of named fields, an instance of which can be treated as a single value. In many languages, composite types are the only kind of user-definable type, and they are by far the most commonly used user-defined type in Julia as well.
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 purer object-oriented languages, such as Ruby or Smalltalk, all values are objects whether they are composites or not. In less pure object oriented languages, including C++ and Java, some values, such as integers and floating-point values, are not objects, while instances of user-defined composite types are true objects with associated methods. In Julia, all values are objects, but functions are 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 ofall of a function's arguments are considered when selecting a method, rather than just the first one (seeMethods for more information on methods and dispatch). Thus, it would be inappropriate for functions to "belong" to only their first argument. Organizing methods into function objects rather than having named bags of methods "inside" each object ends up being a highly beneficial aspect of the language design.
Composite types are introduced with thestruct
keyword followed by a block of field names, optionally annotated with types using the::
operator:
julia> struct Foo bar baz::Int qux::Float64 end
Fields with no type annotation default toAny
, and can accordingly hold any type of value.
New objects of typeFoo
are created by applying theFoo
type object like a function to values for its fields:
julia> foo = Foo("Hello, world.", 23, 1.5)Foo("Hello, world.", 23, 1.5)julia> typeof(foo)Foo
When a type is applied like a function it is called aconstructor. Two constructors are generated automatically (these are calleddefault constructors). One accepts any arguments and callsconvert
to convert them to the types of the fields, and the other accepts arguments that match the field types exactly. The reason both of these are generated is that this makes it easier to add new definitions without inadvertently replacing a default constructor.
Since thebar
field is unconstrained in type, any value will do. However, the value forbaz
must be convertible toInt
:
julia> Foo((), 23.5, 1)ERROR: InexactError: Int64(23.5)Stacktrace:[...]
You may find a list of field names using thefieldnames
function.
julia> fieldnames(Foo)(:bar, :baz, :qux)
You can access the field values of a composite object using the traditionalfoo.bar
notation:
julia> foo.bar"Hello, world."julia> foo.baz23julia> foo.qux1.5
Composite objects declared withstruct
areimmutable; they cannot be modified after construction. This may seem odd at first, but it has several advantages:
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 keywordmutable struct
, to be discussed in the next section.
If all the fields of an immutable structure are indistinguishable (===
) then two immutable values containing those fields are also indistinguishable:
julia> struct X a::Int b::Float64 endjulia> X(1, 2) === X(1, 2)true
There is much more to say about how instances of composite types are created, but that discussion depends on bothParametric Types and onMethods, and is sufficiently important to be addressed in its own section:Constructors.
For many user-defined typesX
, you may want to define a methodBase.broadcastable(x::X) = Ref(x)
so that instances of that type act as 0-dimensional "scalars" forbroadcasting.
If a composite type is declared withmutable struct
instead ofstruct
, then instances of it can be modified:
julia> mutable struct Bar baz qux::Float64 endjulia> bar = Bar("Hello", 1.5);julia> bar.qux = 2.02.0julia> bar.baz = 1//21//2
An extra interface between the fields and the user can be provided throughInstance Properties. This grants more control on what can be accessed and modified using thebar.baz
notation.
In order to support mutation, such objects are generally allocated on the heap, and have stable memory addresses. A mutable object is like a little container that might hold different values over time, and so can only be reliably identified with its address. In contrast, an instance of an immutable type is associated with specific field values –- the field values alone tell you everything about the object. In deciding whether to make a type mutable, ask whether two instances with the same field values would be considered identical, or if they might need to change independently over time. If they would be considered identical, the type should probably be immutable.
To recap, two essential properties define immutability in Julia:
In cases where one or more fields of an otherwise mutable struct is known to be immutable, one can declare these fields as such usingconst
as shown below. This enables some, but not all of the optimizations of immutable structs, and can be used to enforce invariants on the particular fields marked asconst
.
const
annotating fields of mutable structs requires at least Julia 1.8.
julia> mutable struct Baz a::Int const b::Float64 endjulia> baz = Baz(1, 1.5);julia> baz.a = 22julia> baz.b = 2.0ERROR: setfield!: const field .b of type Baz cannot be changed[...]
The three kinds of types (abstract, primitive, composite) discussed in the previous sections are actually all closely related. They share the same key properties:
Because of these shared properties, these types are internally represented as instances of the same concept,DataType
, which is the type of any of these types:
julia> typeof(Real)DataTypejulia> typeof(Int)DataType
ADataType
may be abstract or concrete. If it is concrete, it has a specified size, storage layout, and (optionally) field names. Thus a primitive type is aDataType
with nonzero size, but no field names. A composite type is aDataType
that has field names or is empty (zero size).
Every concrete value in the system is an instance of someDataType
.
A type union is a special abstract type which includes as objects all instances of any of its argument types, constructed using the specialUnion
keyword:
julia> IntOrString = Union{Int,AbstractString}Union{Int64, AbstractString}julia> 1 :: IntOrString1julia> "Hello!" :: IntOrString"Hello!"julia> 1.0 :: IntOrStringERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64
The compilers for many languages have an internal union construct for reasoning about types; Julia simply exposes it to the programmer. The Julia compiler is able to generate efficient code in the presence ofUnion
types with a small number of types[1], by generating specialized code in separate branches for each possible type.
A particularly useful case of aUnion
type isUnion{T, Nothing}
, whereT
can be any type andNothing
is the singleton type whose only instance is the objectnothing
. This pattern is the Julia equivalent ofNullable
,Option
orMaybe
types in other languages. Declaring a function argument or a field asUnion{T, Nothing}
allows setting it either to a value of typeT
, or tonothing
to indicate that there is no value. Seethis FAQ entry for more information.
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. There are many languages that support some version ofgeneric programming, wherein data structures and algorithms to manipulate them may be specified without specifying the exact types involved. For example, some form of generic programming exists in ML, Haskell, Ada, Eiffel, C++, Java, C#, F#, and Scala, just to name a few. Some of these languages support true parametric polymorphism (e.g. ML, Haskell, Scala), while others support ad-hoc, template-based styles of generic programming (e.g. C++, Java). With so many different varieties of generic programming and parametric types in various languages, we won't even attempt to compare Julia's parametric types to other languages, but will instead focus on explaining Julia's system in its own right. We will note, however, that because Julia is a dynamically typed language and doesn't need to make all type decisions at compile time, many traditional difficulties encountered in static parametric type systems can be relatively easily handled.
All declared types (theDataType
variety) can be parameterized, with the same syntax in each case. We will discuss them in the following order: first, parametric composite types, then parametric abstract types, and finally parametric primitive types.
Type parameters are introduced immediately after the type name, surrounded by curly braces:
julia> struct Point{T} x::T y::T end
This declaration defines a new parametric type,Point{T}
, holding two "coordinates" of typeT
. What, one may ask, isT
? Well, that's precisely the point of parametric types: it can be any type at all (or a value of any bits type, actually, although here it's clearly used as a type).Point{Float64}
is a concrete type equivalent to the type defined by replacingT
in the definition ofPoint
withFloat64
. Thus, this single declaration actually declares an unlimited number of types:Point{Float64}
,Point{AbstractString}
,Point{Int64}
, etc. Each of these is now a usable concrete type:
julia> Point{Float64}Point{Float64}julia> Point{AbstractString}Point{AbstractString}
The typePoint{Float64}
is a point whose coordinates are 64-bit floating-point values, while the typePoint{AbstractString}
is a "point" whose "coordinates" are string objects (seeStrings).
Point
itself is also a valid type object, containing all instancesPoint{Float64}
,Point{AbstractString}
, etc. as subtypes:
julia> Point{Float64} <: Pointtruejulia> Point{AbstractString} <: Pointtrue
Other types, of course, are not subtypes of it:
julia> Float64 <: Pointfalsejulia> AbstractString <: Pointfalse
ConcretePoint
types with different values ofT
are never subtypes of each other:
julia> Point{Float64} <: Point{Int64}falsejulia> Point{Float64} <: Point{Real}false
This last point isvery important: even thoughFloat64 <: Real
weDO NOT havePoint{Float64} <: Point{Real}
.
In other words, in the parlance of type theory, Julia's type parameters areinvariant, rather than beingcovariant (or even contravariant). This is for practical reasons: while any instance ofPoint{Float64}
may conceptually be like an instance ofPoint{Real}
as well, the two types have different representations in memory:
Point{Float64}
can be represented compactly and efficiently as an immediate pair of 64-bit values;Point{Real}
must be able to hold any pair of instances ofReal
. Since objects that are instances ofReal
can be of arbitrary size and structure, in practice an instance ofPoint{Real}
must be represented as a pair of pointers to individually allocatedReal
objects.The efficiency gained by being able to storePoint{Float64}
objects with immediate values is magnified enormously in the case of arrays: anArray{Float64}
can be stored as a contiguous memory block of 64-bit floating-point values, whereas anArray{Real}
must be an array of pointers to individually allocatedReal
objects – which may well beboxed 64-bit floating-point values, but also might be arbitrarily large, complex objects, which are declared to be implementations of theReal
abstract type.
SincePoint{Float64}
is not a subtype ofPoint{Real}
, the following method can't be applied to arguments of typePoint{Float64}
:
function norm(p::Point{Real}) sqrt(p.x^2 + p.y^2)end
A correct way to define a method that accepts all arguments of typePoint{T}
whereT
is a subtype ofReal
is:
function norm(p::Point{<:Real}) sqrt(p.x^2 + p.y^2)end
(Equivalently, one could definefunction norm(p::Point{T} where T<:Real)
orfunction norm(p::Point{T}) where T<:Real
; seeUnionAll Types.)
More examples will be discussed later inMethods.
How does one construct aPoint
object? It is possible to define custom constructors for composite types, which will be discussed in detail inConstructors, but in the absence of any special constructor declarations, there are two default ways of creating new composite objects, one in which the type parameters are explicitly given and the other in which they are implied by the arguments to the object constructor.
Since the typePoint{Float64}
is a concrete type equivalent toPoint
declared withFloat64
in place ofT
, it can be applied as a constructor accordingly:
julia> p = Point{Float64}(1.0, 2.0)Point{Float64}(1.0, 2.0)julia> typeof(p)Point{Float64}
For the default constructor, exactly one argument must be supplied for each field:
julia> Point{Float64}(1.0)ERROR: MethodError: no method matching Point{Float64}(::Float64)The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.[...]julia> Point{Float64}(1.0, 2.0, 3.0)ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.[...]
Only one default constructor is generated for parametric types, since overriding it is not possible. This constructor accepts any arguments and converts them to the field types.
In many cases, it is redundant to provide the type ofPoint
object one wants to construct, since the types of arguments to the constructor call already implicitly provide type information. For that reason, you can also applyPoint
itself as a constructor, provided that the implied value of the parameter typeT
is unambiguous:
julia> p1 = Point(1.0,2.0)Point{Float64}(1.0, 2.0)julia> typeof(p1)Point{Float64}julia> p2 = Point(1,2)Point{Int64}(1, 2)julia> typeof(p2)Point{Int64}
In the case ofPoint
, the type ofT
is unambiguously implied if and only if the two arguments toPoint
have the same type. When this isn't the case, the constructor will fail with aMethodError
:
julia> Point(1,2.5)ERROR: MethodError: no method matching Point(::Int64, ::Float64)The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.Closest candidates are: Point(::T, !Matched::T) where T @ Main none:2Stacktrace:[...]
Constructor methods to appropriately handle such mixed cases can be defined, but that will not be discussed until later on inConstructors.
Parametric abstract type declarations declare a collection of abstract types, in much the same way:
julia> abstract type Pointy{T} end
With this declaration,Pointy{T}
is a distinct abstract type for each type or integer value ofT
. As with parametric composite types, each such instance is a subtype ofPointy
:
julia> Pointy{Int64} <: Pointytruejulia> Pointy{1} <: Pointytrue
Parametric abstract types are invariant, much as parametric composite types are:
julia> Pointy{Float64} <: Pointy{Real}falsejulia> Pointy{Real} <: Pointy{Float64}false
The notationPointy{<:Real}
can be used to express the Julia analogue of acovariant type, whilePointy{>:Int}
the analogue of acontravariant type, but technically these representsets of types (seeUnionAll Types).
julia> Pointy{Float64} <: Pointy{<:Real}truejulia> Pointy{Real} <: Pointy{>:Int}true
Much as plain old abstract types serve to create a useful hierarchy of types over concrete types, parametric abstract types serve the same purpose with respect to parametric composite types. We could, for example, have declaredPoint{T}
to be a subtype ofPointy{T}
as follows:
julia> struct Point{T} <: Pointy{T} x::T y::T end
Given such a declaration, for each choice ofT
, we havePoint{T}
as a subtype ofPointy{T}
:
julia> Point{Float64} <: Pointy{Float64}truejulia> Point{Real} <: Pointy{Real}truejulia> Point{AbstractString} <: Pointy{AbstractString}true
This relationship is also invariant:
julia> Point{Float64} <: Pointy{Real}falsejulia> Point{Float64} <: Pointy{<:Real}true
What purpose do parametric abstract types likePointy
serve? Consider if we create a point-like implementation that only requires a single coordinate because the point is on the diagonal linex = y:
julia> struct DiagPoint{T} <: Pointy{T} x::T end
Now bothPoint{Float64}
andDiagPoint{Float64}
are implementations of thePointy{Float64}
abstraction, and similarly for every other possible choice of typeT
. This allows programming to a common interface shared by allPointy
objects, implemented for bothPoint
andDiagPoint
. This cannot be fully demonstrated, however, until we have introduced methods and dispatch in the next section,Methods.
There are situations where it may not make sense for type parameters to range freely over all possible types. In such situations, one can constrain the range ofT
like so:
julia> abstract type Pointy{T<:Real} end
With such a declaration, it is acceptable to use any type that is a subtype ofReal
in place ofT
, but not types that are not subtypes ofReal
:
julia> Pointy{Float64}Pointy{Float64}julia> Pointy{Real}Pointy{Real}julia> Pointy{AbstractString}ERROR: TypeError: in Pointy, in T, expected T<:Real, got Type{AbstractString}julia> Pointy{1}ERROR: TypeError: in Pointy, in T, expected T<:Real, got a value of type Int64
Type parameters for parametric composite types can be restricted in the same manner:
struct Point{T<:Real} <: Pointy{T} x::T y::Tend
To give a real-world example of how all this parametric type machinery can be useful, here is the actual definition of Julia'sRational
immutable type (except that we omit the constructor here for simplicity), representing an exact ratio of integers:
struct Rational{T<:Integer} <: Real num::T den::Tend
It only makes sense to take ratios of integer values, so the parameter typeT
is restricted to being a subtype ofInteger
, and a ratio of integers represents a value on the real number line, so anyRational
is an instance of theReal
abstraction.
Tuples are an abstraction of the arguments of a function – without the function itself. The salient aspects of a function's arguments are their order and their types. Therefore a tuple type is similar to a parameterized immutable type where each parameter is the type of one field. For example, a 2-element tuple type resembles the following immutable type:
struct Tuple2{A,B} a::A b::Bend
However, there are three key differences:
Tuple{Int}
is a subtype ofTuple{Any}
. ThereforeTuple{Any}
is considered an abstract type, and tuple types are only concrete if their parameters are.Tuple values are written with parentheses and commas. When a tuple is constructed, an appropriate tuple type is generated on demand:
julia> typeof((1,"foo",2.5))Tuple{Int64, String, Float64}
Note the implications of covariance:
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}truejulia> Tuple{Int,AbstractString} <: Tuple{Real,Real}falsejulia> Tuple{Int,AbstractString} <: Tuple{Real,}false
Intuitively, this corresponds to the type of a function's arguments being a subtype of the function's signature (when the signature matches).
The last parameter of a tuple type can be the special valueVararg
, which denotes any number of trailing elements:
julia> mytupletype = Tuple{AbstractString,Vararg{Int}}Tuple{AbstractString, Vararg{Int64}}julia> isa(("1",), mytupletype)truejulia> isa(("1",1), mytupletype)truejulia> isa(("1",1,2), mytupletype)truejulia> isa(("1",1,2,3.0), mytupletype)false
MoreoverVararg{T}
corresponds to zero or more elements of typeT
. Vararg tuple types are used to represent the arguments accepted by varargs methods (seeVarargs Functions).
The special valueVararg{T,N}
(when used as the last parameter of a tuple type) corresponds to exactlyN
elements of typeT
.NTuple{N,T}
is a convenient alias forTuple{Vararg{T,N}}
, i.e. a tuple type containing exactlyN
elements of typeT
.
Named tuples are instances of theNamedTuple
type, which has two parameters: a tuple of symbols giving the field names, and a tuple type giving the field types. For convenience,NamedTuple
types are printed using the@NamedTuple
macro which provides a convenientstruct
-like syntax for declaring these types viakey::Type
declarations, where an omitted::Type
corresponds to::Any
.
julia> typeof((a=1,b="hello")) # prints in macro form@NamedTuple{a::Int64, b::String}julia> NamedTuple{(:a, :b), Tuple{Int64, String}} # long form of the type@NamedTuple{a::Int64, b::String}
Thebegin ... end
form of the@NamedTuple
macro allows the declarations to be split across multiple lines (similar to a struct declaration), but is otherwise equivalent:
julia> @NamedTuple begin a::Int b::String end@NamedTuple{a::Int64, b::String}
ANamedTuple
type can be used as a constructor, accepting a single tuple argument. The constructedNamedTuple
type can be either a concrete type, with both parameters specified, or a type that specifies only field names:
julia> @NamedTuple{a::Float32,b::String}((1, ""))(a = 1.0f0, b = "")julia> NamedTuple{(:a, :b)}((1, ""))(a = 1, b = "")
If field types are specified, the arguments are converted. Otherwise the types of the arguments are used directly.
Primitive types can also be declared parametrically. For example, pointers are represented as primitive types which would be declared in Julia like this:
# 32-bit system:primitive type Ptr{T} 32 end# 64-bit system:primitive type Ptr{T} 64 end
The slightly odd feature of these declarations as compared to typical parametric composite types, is that the type parameterT
is not used in the definition of the type itself – it is just an abstract tag, essentially defining an entire family of types with identical structure, differentiated only by their type parameter. Thus,Ptr{Float64}
andPtr{Int64}
are distinct types, even though they have identical representations. And of course, all specific pointer types are subtypes of the umbrellaPtr
type:
julia> Ptr{Float64} <: Ptrtruejulia> Ptr{Int64} <: Ptrtrue
We have said that a parametric type likePtr
acts as a supertype of all its instances (Ptr{Int64}
etc.). How does this work?Ptr
itself cannot be a normal data type, since without knowing the type of the referenced data the type clearly cannot be used for memory operations. The answer is thatPtr
(or other parametric types likeArray
) is a different kind of type called aUnionAll
type. Such a type expresses theiterated union of types for all values of some parameter.
UnionAll
types are usually written using the keywordwhere
. For examplePtr
could be more accurately written asPtr{T} where T
, meaning all values whose type isPtr{T}
for some value ofT
. In this context, the parameterT
is also often called a "type variable" since it is like a variable that ranges over types. Eachwhere
introduces a single type variable, so these expressions are nested for types with multiple parameters, for exampleArray{T,N} where N where T
.
The type application syntaxA{B,C}
requiresA
to be aUnionAll
type, and first substitutesB
for the outermost type variable inA
. The result is expected to be anotherUnionAll
type, into whichC
is then substituted. SoA{B,C}
is equivalent toA{B}{C}
. This explains why it is possible to partially instantiate a type, as inArray{Float64}
: the first parameter value has been fixed, but the second still ranges over all possible values. Using explicitwhere
syntax, any subset of parameters can be fixed. For example, the type of all 1-dimensional arrays can be written asArray{T,1} where T
.
Type variables can be restricted with subtype relations.Array{T} where T<:Integer
refers to all arrays whose element type is some kind ofInteger
. The syntaxArray{<:Integer}
is a convenient shorthand forArray{T} where T<:Integer
. Type variables can have both lower and upper bounds.Array{T} where Int<:T<:Number
refers to all arrays ofNumber
s that are able to containInt
s (sinceT
must be at least as big asInt
). The syntaxwhere T>:Int
also works to specify only the lower bound of a type variable, andArray{>:Int}
is equivalent toArray{T} where T>:Int
.
Sincewhere
expressions nest, type variable bounds can refer to outer type variables. For exampleTuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real
refers to 2-tuples whose first element is someReal
, and whose second element is anArray
of any kind of array whose element type contains the type of the first tuple element.
Thewhere
keyword itself can be nested inside a more complex declaration. For example, consider the two types created by the following declarations:
julia> const T1 = Array{Array{T, 1} where T, 1}Vector{Vector} (alias for Array{Array{T, 1} where T, 1})julia> const T2 = Array{Array{T, 1}, 1} where TArray{Vector{T}, 1} where T
TypeT1
defines a 1-dimensional array of 1-dimensional arrays; each of the inner arrays consists of objects of the same type, but this type may vary from one inner array to the next. On the other hand, typeT2
defines a 1-dimensional array of 1-dimensional arrays all of whose inner arrays must have the same type. Note thatT2
is an abstract type, e.g.,Array{Array{Int,1},1} <: T2
, whereasT1
is a concrete type. As a consequence,T1
can be constructed with a zero-argument constructora=T1()
butT2
cannot.
There is a convenient syntax for naming such types, similar to the short form of function definition syntax:
Vector{T} = Array{T, 1}
This is equivalent toconst Vector = Array{T,1} where T
. WritingVector{Float64}
is equivalent to writingArray{Float64,1}
, and the umbrella typeVector
has as instances allArray
objects where the second parameter – the number of array dimensions – is 1, regardless of what the element type is. In languages where parametric types must always be specified in full, this is not especially helpful, but in Julia, this allows one to write justVector
for the abstract type including all one-dimensional dense arrays of any element type.
Immutable composite types with no fields are calledsingletons. Formally, if
T
is an immutable composite type (i.e. defined withstruct
),a isa T && b isa T
impliesa === b
,thenT
is a singleton type.[2]Base.issingletontype
can be used to check if a type is a singleton type.Abstract types cannot be singleton types by construction.
From the definition, it follows that there can be only one instance of such types:
julia> struct NoFields endjulia> NoFields() === NoFields()truejulia> Base.issingletontype(NoFields)true
The===
function confirms that the constructed instances ofNoFields
are actually one and the same.
Parametric types can be singleton types when the above condition holds. For example,
julia> struct NoFieldsParam{T} endjulia> Base.issingletontype(NoFieldsParam) # Can't be a singleton type ...falsejulia> NoFieldsParam{Int}() isa NoFieldsParam # ... because it has ...truejulia> NoFieldsParam{Bool}() isa NoFieldsParam # ... multiple instances.truejulia> Base.issingletontype(NoFieldsParam{Int}) # Parametrized, it is a singleton.truejulia> NoFieldsParam{Int}() === NoFieldsParam{Int}()true
Each function has its own type, which is a subtype ofFunction
.
julia> foo41(x) = x + 1foo41 (generic function with 1 method)julia> typeof(foo41)typeof(foo41) (singleton type of function foo41, subtype of Function)
Note howtypeof(foo41)
prints as itself. This is merely a convention for printing, as it is a first-class object that can be used like any other value:
julia> T = typeof(foo41)typeof(foo41) (singleton type of function foo41, subtype of Function)julia> T <: Functiontrue
Types of functions defined at top-level are singletons. When necessary, you can compare them with===
.
Closures also have their own type, which is usually printed with names that end in#<number>
. Names and types for functions defined at different locations are distinct, but not guaranteed to be printed the same way across sessions.
julia> typeof(x -> x + 1)var"#9#10"
Types of closures are not necessarily singletons.
julia> addy(y) = x -> x + yaddy (generic function with 1 method)julia> typeof(addy(1)) === typeof(addy(2))truejulia> addy(1) === addy(2)falsejulia> Base.issingletontype(typeof(addy(1)))false
Type{T}
type selectorsFor each typeT
,Type{T}
is an abstract parametric type whose only instance is the objectT
. Until we discussParametric Methods andconversions, it is difficult to explain the utility of this construct, but in short, it allows one to specialize function behavior on specific types asvalues. This is useful for writing methods (especially parametric ones) whose behavior depends on a type that is given as an explicit argument rather than implied by the type of one of its arguments.
Since the definition is a little difficult to parse, let's look at some examples:
julia> isa(Float64, Type{Float64})truejulia> isa(Real, Type{Float64})falsejulia> isa(Real, Type{Real})truejulia> isa(Float64, Type{Real})false
In other words,isa(A, Type{B})
is true if and only ifA
andB
are the same object and that object is a type.
In particular, since parametric types areinvariant, we have
julia> struct TypeParamExample{T} x::T endjulia> TypeParamExample isa Type{TypeParamExample}truejulia> TypeParamExample{Int} isa Type{TypeParamExample}falsejulia> TypeParamExample{Int} isa Type{TypeParamExample{Int}}true
Without the parameter,Type
is simply an abstract type which has all type objects as its instances:
julia> isa(Type{Float64}, Type)truejulia> isa(Float64, Type)truejulia> isa(Real, Type)true
Any object that is not a type is not an instance ofType
:
julia> isa(1, Type)falsejulia> isa("foo", Type)false
WhileType
is part of Julia's type hierarchy like any other abstract parametric type, it is not commonly used outside method signatures except in some special cases. Another important use case forType
is sharpening field types which would otherwise be captured less precisely, e.g. asDataType
in the example below where the default constructor could lead to performance problems in code relying on the precise wrapped type (similarly toabstract type parameters).
julia> struct WrapType{T} value::T endjulia> WrapType(Float64) # default constructor, note DataTypeWrapType{DataType}(Float64)julia> WrapType(::Type{T}) where T = WrapType{Type{T}}(T)WrapTypejulia> WrapType(Float64) # sharpened constructor, note more precise Type{Float64}WrapType{Type{Float64}}(Float64)
Sometimes it is convenient to introduce a new name for an already expressible type. This can be done with a simple assignment statement. For example,UInt
is aliased to eitherUInt32
orUInt64
as is appropriate for the size of pointers on the system:
# 32-bit system:julia> UIntUInt32# 64-bit system:julia> UIntUInt64
This is accomplished via the following code inbase/boot.jl
:
if Int === Int64 const UInt = UInt64else const UInt = UInt32end
Of course, this depends on whatInt
is aliased to – but that is predefined to be the correct type – eitherInt32
orInt64
.
(Note that unlikeInt
,Float
does not exist as a type alias for a specific sizedAbstractFloat
. Unlike with integer registers, where the size ofInt
reflects the size of a native pointer on that machine, the floating point register sizes are specified by the IEEE-754 standard.)
Type aliases may be parametrized:
julia> const Family{T} = Set{T}Setjulia> Family{Char} === Set{Char}true
Since types in Julia are themselves objects, ordinary functions can operate on them. Some functions that are particularly useful for working with or exploring types have already been introduced, such as the<:
operator, which indicates whether its left hand operand is a subtype of its right hand operand.
Theisa
function tests if an object is of a given type and returns true or false:
julia> isa(1, Int)truejulia> isa(1, AbstractFloat)false
Thetypeof
function, already used throughout the manual in examples, returns the type of its argument. Since, as noted above, types are objects, they also have types, and we can ask what their types are:
julia> typeof(Rational{Int})DataTypejulia> typeof(Union{Real,String})Union
What if we repeat the process? What is the type of a type of a type? As it happens, types are all composite values and thus all have a type ofDataType
:
julia> typeof(DataType)DataTypejulia> typeof(Union)DataType
DataType
is its own type.
Another operation that applies to some types issupertype
, which reveals a type's supertype. Only declared types (DataType
) have unambiguous supertypes:
julia> supertype(Float64)AbstractFloatjulia> supertype(Number)Anyjulia> supertype(AbstractString)Anyjulia> supertype(Any)Any
If you applysupertype
to other type objects (or non-type objects), aMethodError
is raised:
julia> supertype(Union{Float64,Int64})ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})The function `supertype` exists, but no method is defined for this combination of argument types.Closest candidates are:[...]
Often, one wants to customize how instances of a type are displayed. This is accomplished by overloading theshow
function. For example, suppose we define a type to represent complex numbers in polar form:
julia> struct Polar{T<:Real} <: Number r::T Θ::T endjulia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)Polar
Here, we've added a custom constructor function so that it can take arguments of differentReal
types and promote them to a common type (seeConstructors andConversion and Promotion). (Of course, we would have to define lots of other methods, too, to make it act like aNumber
, e.g.+
,*
,one
,zero
, promotion rules and so on.) By default, instances of this type display rather simply, with information about the type name and the field values, as e.g.Polar{Float64}(3.0,4.0)
.
If we want it to display instead as3.0 * exp(4.0im)
, we would define the following method to print the object to a given output objectio
(representing a file, terminal, buffer, etcetera; seeNetworking and Streams):
julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")
More fine-grained control over display ofPolar
objects is possible. In particular, sometimes one wants both a verbose multi-line printing format, used for displaying a single object in the REPL and other interactive environments, and also a more compact single-line format used forprint
or for displaying the object as part of another object (e.g. in an array). Although by default theshow(io, z)
function is called in both cases, you can define adifferent multi-line format for displaying an object by overloading a three-argument form ofshow
that takes thetext/plain
MIME type as its second argument (seeMultimedia I/O), for example:
julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} = print(io, "Polar{$T} complex number:\n ", z)
(Note thatprint(..., z)
here will call the 2-argumentshow(io, z)
method.) This results in:
julia> Polar(3, 4.0)Polar{Float64} complex number: 3.0 * exp(4.0im)julia> [Polar(3, 4.0), Polar(4.0,5.3)]2-element Vector{Polar{Float64}}: 3.0 * exp(4.0im) 4.0 * exp(5.3im)
where the single-lineshow(io, z)
form is still used for an array ofPolar
values. Technically, the REPL callsdisplay(z)
to display the result of executing a line, which defaults toshow(stdout, MIME("text/plain"), z)
, which in turn defaults toshow(stdout, z)
, but you shouldnot define newdisplay
methods unless you are defining a new multimedia display handler (seeMultimedia I/O).
Moreover, you can also defineshow
methods for other MIME types in order to enable richer display (HTML, images, etcetera) of objects in environments that support this (e.g. IJulia). For example, we can define formatted HTML display ofPolar
objects, with superscripts and italics, via:
julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} = println(io, "<code>Polar{$T}</code> complex number: ", z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")
APolar
object will then display automatically using HTML in an environment that supports HTML display, but you can callshow
manually to get HTML output if you want:
julia> show(stdout, "text/html", Polar(3.0,4.0))<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>
An HTML renderer would display this as:Polar{Float64}
complex number: 3.0e4.0i
As a rule of thumb, the single-lineshow
method should print a valid Julia expression for creating the shown object. When thisshow
method contains infix operators, such as the multiplication operator (*
) in our single-lineshow
method forPolar
above, it may not parse correctly when printed as part of another object. To see this, consider the expression object (seeProgram representation) which takes the square of a specific instance of ourPolar
type:
julia> a = Polar(3, 4.0)Polar{Float64} complex number: 3.0 * exp(4.0im)julia> print(:($a^2))3.0 * exp(4.0im) ^ 2
Because the operator^
has higher precedence than*
(seeOperator Precedence and Associativity), this output does not faithfully represent the expressiona ^ 2
which should be equal to(3.0 * exp(4.0im)) ^ 2
. To solve this issue, we must make a custom method forBase.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int)
, which is called internally by the expression object when printing:
julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int) if Base.operator_precedence(:*) <= precedence print(io, "(") show(io, z) print(io, ")") else show(io, z) end endjulia> :($a^2):((3.0 * exp(4.0im)) ^ 2)
The method defined above adds parentheses around the call toshow
when the precedence of the calling operator is higher than or equal to the precedence of multiplication. This check allows expressions which parse correctly without the parentheses (such as:($a + 2)
and:($a == 2)
) to omit them when printing:
julia> :($a + 2):(3.0 * exp(4.0im) + 2)julia> :($a == 2):(3.0 * exp(4.0im) == 2)
In some cases, it is useful to adjust the behavior ofshow
methods depending on the context. This can be achieved via theIOContext
type, which allows passing contextual properties together with a wrapped IO stream. For example, we can build a shorter representation in ourshow
method when the:compact
property is set totrue
, falling back to the long representation if the property isfalse
or absent:
julia> function Base.show(io::IO, z::Polar) if get(io, :compact, false)::Bool print(io, z.r, "ℯ", z.Θ, "im") else print(io, z.r, " * exp(", z.Θ, "im)") end end
This new compact representation will be used when the passed IO stream is anIOContext
object with the:compact
property set. In particular, this is the case when printing arrays with multiple columns (where horizontal space is limited):
julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))3.0ℯ4.0imjulia> [Polar(3, 4.0) Polar(4.0,5.3)]1×2 Matrix{Polar{Float64}}: 3.0ℯ4.0im 4.0ℯ5.3im
See theIOContext
documentation for a list of common properties which can be used to adjust printing.
In Julia, you can't dispatch on avalue such astrue
orfalse
. However, you can dispatch on parametric types, and Julia allows you to include "plain bits" values (Types, Symbols, Integers, floating-point numbers, tuples, etc.) as type parameters. A common example is the dimensionality parameter inArray{T,N}
, whereT
is a type (e.g.,Float64
) butN
is just anInt
.
You can create your own custom types that take values as parameters, and use them to control dispatch of custom types. By way of illustration of this idea, let's introduce the parametric typeVal{x}
, and its constructorVal(x) = Val{x}()
, which serves as a customary way to exploit this technique for cases where you don't need a more elaborate hierarchy.
Val
is defined as:
julia> struct Val{x} endjulia> Val(x) = Val{x}()Val
There is no more to the implementation ofVal
than this. Some functions in Julia's standard library acceptVal
instances as arguments, and you can also use it to write your own functions. For example:
julia> firstlast(::Val{true}) = "First"firstlast (generic function with 1 method)julia> firstlast(::Val{false}) = "Last"firstlast (generic function with 2 methods)julia> firstlast(Val(true))"First"julia> firstlast(Val(false))"Last"
For consistency across Julia, the call site should always pass aVal
instance rather than using atype, i.e., usefoo(Val(:bar))
rather thanfoo(Val{:bar})
.
It's worth noting that it's extremely easy to mis-use parametric "value" types, includingVal
; in unfavorable cases, you can easily end up making the performance of your code muchworse. In particular, you would never want to write actual code as illustrated above. For more information about the proper (and improper) uses ofVal
, please readthe more extensive discussion in the performance tips.
Settings
This document was generated withDocumenter.jl version 1.8.0 onWednesday 9 July 2025. Using Julia version 1.11.6.