Types
Type systems are traditionally divided into two quite different categories: static, in which the type of each expression is determined before executing the program, and dynamic, in which nothing is known about the types until execution, until the actual values become available. In languages with static typing, the object approach allows for some flexibility: when writing code, it is not necessary to know exactly the types of values at the time of compilation. The phenomenon when the same code can operate on different types is called polymorphism. In classical dynamically typed languages, all code is polymorphic: value types are limited only when explicitly checking types or if operations on objects are not supported at runtime.
Julia’s type system is dynamic, but it has some of the advantages of static systems.: you can specify that an object has a specific type. This not only helps to create more efficient code, but also, more importantly, allows deep integration of method dispatching based on argument types into the language. Dispatching methods is discussed in detail in the chapter Methods, but it has its roots in the type system described here.
If the value type is not specified in Julia, then by default it is assumed that the type can be any. Thus, in Julia, you can write a variety of functions without explicitly using types at all. However, if you need to make the behavior of a function more explicit, you can gradually add explicit type annotations to previously untyped code. Adding annotations has three main goals: using Julia’s efficient multiple dispatch mechanism, improving code readability, and intercepting programmer errors.
In terms of https://en.wikipedia.org/wiki/Type_system [type systems] The Julia language can be described as dynamic, nominative, and parametric. Universal types can be parameterized, and hierarchical relationships between types can be https://en.wikipedia.org/wiki/Nominal_type_system [are explicitly declared] rather than https://en.wikipedia.org/wiki/Structural_type_system [are implied based on the structure]. An important distinguishing feature of the Julia type system is that concrete types cannot be subtypes of each other: all concrete types are finite, and only abstract types can be their supertypes. Although at first glance this restriction may seem unnecessarily strict, it has many positive effects and a surprisingly small number of disadvantages. It turns out that the ability to inherit behavior is much more important than the ability to inherit structure, and inheriting both creates serious difficulties in traditional object-oriented languages. In addition, we should immediately mention a number of other important aspects of the Julia type system.
-
Values are not divided into object and non-object: all values in Julia are true objects whose types belong to a single fully connected type graph. All nodes in this graph are equal types.
-
The concept of "compile-time type" in Julia does not make sense: the only type that a value can have is its actual type at the time of program execution. In object-oriented languages, this is called the runtime type, and due to the combination of static compilation and polymorphism, this difference is essential.
-
Only values have types, not variables: a variable is just a name associated with a value, although for brevity you can say "type of variable" instead of "type of value referenced by the variable."
-
Both abstract and concrete types can be parameterized by other types. They can also be parameterized by symbols, values for which the function 'isbits' returns true (in particular, numeric and boolean values that are stored as C types or structures (
struct
) without pointers to other objects), as well as tuples of such values. If type parameters do not need to be referenced or restricted, then they can be omitted.
The Julia type system is designed to be effective and expressive, but at the same time clearly organized, intuitive and unobtrusive. Many Julia programmers don’t have to write code at all in which types are explicitly applied. However, in some cases, declaring types allows you to make the code clearer, simpler, faster, and more reliable.
Type Declarations
Using the operator ::
you can add type annotations to expressions and variables in the code. There may be two main reasons for this:
-
A statement that helps ensure the correct operation of the program.
-
Providing additional type information to the compiler, which can be used to improve performance in some cases.
When added to an expression intended for calculating a value, the operator ::
means "is an instance of the type". Thus, he claims that the value of the expression on the left side is an instance of the type on the right. If the type on the right is specific, the value on the left must be implemented by that type. Recall that all concrete types are finite, so an implementation of a type cannot be a subtype of another implementation. If the type on the right is abstract, it is sufficient that the value is implemented by a specific type, which is a subtype of this abstract type. If the type statement is not true, an exception is thrown; otherwise, the value of the expression on the left is returned.
julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64
julia> (1+2)::Int
3
This allows you to add a type statement to any expression in place.
When added to a variable on the left side of an assignment or as part of a local
declaration, the ::
operator has a slightly different meaning: it declares that the variable always has the specified type, similar to type declarations in statically typed languages such as C. Any value assigned to a variable is converted to the declared type using the function convert
.
julia> function foo()
x::Int8 = 100
x
end
foo (generic function with 1 method)
julia> x = foo()
100
julia> typeof(x)
Int8
This application allows you to avoid performance problems as a result of assigning a variable of an unexpected type.
This option (type declaration) is used only in certain situations.:
local x::Int8 # в объявлении local
x::Int8 = 10 # в левой части присваивания
and it applies to the entire current area even before the announcement.
Starting with Julia 1.8, type declarations can be used in the global scope, meaning that type annotations can be added to global variables so that they can be accessed in a type-stable manner.
julia> x::Int = 10
10
julia> x = 3.5
ERROR: InexactError: Int64(3.5)
julia> function foo(y)
global x = 15.8 # при вызове foo выдается ошибка
return x + y
end
foo (generic function with 1 method)
julia> foo(10)
ERROR: InexactError: Int64(15.8)
Declarations can also be applied to function definitions.
function sinc(x)::Float64
if x == 0
return 1
end
return sin(pi*x)/(pi*x)
end
When this function returns a value, the same thing happens as when assigning a value to a variable with a declared type: the value is always converted to the Float64
type.
Abstract types
Abstract types do not allow instantiation, but simply serve as nodes in the type graph, describing sets of related concrete types - types that are descendants of this abstract type. Although abstract types do not involve instantiation, we will start with them, since they are the backbone of the type system. They form a conceptual hierarchy, thanks to which the Julia type system is more than just a set of object implementations.
As you may remember, in the chapter Integers and floating point numbers various specific types of numeric values were introduced: Int8
, UInt8
, Int16
, UInt16
, Int32
, UInt32
, Int64
, UInt64
, Int128
, UInt128
, Float16
, Float32
and Float64
. Although the types Int8
, Int16
, Int32
, Int64
and Int128
have different representation sizes, what unites them is that they are all signed integer types. In turn, UInt8
, UInt16
, UInt32
, UInt64
and UInt128
are unsigned integer types, while Float16
, Float32
and Float64
are floating-point types. Often, in a certain part of the code, it may be important that the arguments are, for example, some integers, but it doesn’t matter which ones are named. So, the algorithm for finding the greatest common divisor will work with any integers, but not with floating-point numbers. Abstract types allow you to build a hierarchy of types into which specific types fit. For example, it makes it easy to write an algorithm that will work with any integer type without restrictions.
Abstract types are declared using a keyword abstract type
. The standard syntactic constructions for declaring an abstract type look like this:
abstract type «name» end abstract type «name» <: «supertype» end
The keyword abstract type
introduces a new abstract type named "name"
. After this name, you can specify <:
and any existing type. This means that the abstract type being declared is a subtype of this parent type.
If no supertype is specified, by default it is Any
— a predefined abstract type, of which all objects are instances, and all other types are subtypes. In type theory, the type Any
is usually called the highest type because it is located at the very top of the type graph. Julia also has a predefined abstract base type, which is located at the lowest point of the type graph. It is written as Union{}
. Этот тип является прямой противоположностью типу Any
: ни один объект не является экземпляром Union{}
, and all types are supertypes of `Union{}'.
Let’s consider a number of abstract types that make up the Julia numeric type hierarchy.
abstract type Number end
abstract type Real <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer <: Real end
abstract type Signed <: Integer end
abstract type Unsigned <: Integer end
Type Number
is a direct descendant of type Any', and `Real
is its descendant. In turn, the Real
has two descendants (actually there are more of them, but only two are presented here; we will return to the rest later). Integer
and AbstractFloat
. They represent integers and real numbers, respectively. Representations of real numbers include floating-point types, but there are also other types, such as rational ones. 'AbstractFloat` only includes representations of floating`point real numbers. Integer types are further subdivided into Signed
(with a sign) and Unsigned
(unsigned).
The operator `<: in a general sense, it means "is a subtype" and declares that the type on the right is a direct supertype of the type being declared. It can also be used as a subtype operator in expressions that return `true' if the left operand is a subtype of the right one.
julia> Integer <: Number
true
julia> Integer <: AbstractFloat
false
An important purpose of abstract types is to provide default implementations for specific types. Here is a simple example.
function myplus(x,y)
x+y
end
First, you should pay attention to the fact that the above argument declarations are equivalent to x::Any
and y::Any
. When calling this function, for example, myplus(2,5)
, the dispatcher selects the most specific method named myplus
, corresponding to the arguments passed. (For more information about multiple dispatching, see the chapter Methods.)
If a method more specific than the above is not found, Julia creates an internal definition and compiles a method named myplus
for two arguments of type Int
based on the above universal function. In other words, the following method is implicitly defined and compiled.
function myplus(x::Int,y::Int)
x+y
end
Finally, this specific method is called.
Thus, abstract types allow developers to write universal functions that can later be used as default methods for various combinations of specific types. Thanks to multiple dispatching, the programmer can fully control which method is used: the default method or a more specific one.
It is important to note that using a function with arguments of abstract types does not affect performance, because the function is compiled repeatedly for each tuple of arguments of specific types with which it is called. (However, performance problems are possible if the function arguments are containers of abstract types; see the chapter Performance Tips.)
Primitive types
It is almost always preferable to enclose an existing primitive type in a new composite type rather than defining your own primitive type. |
This feature is necessary so that the Julia environment can initialize the standard primitive types supported by LLVM. After their identification, there are practically no grounds for determining additional such types.
A primitive type is a specific type consisting of ordinary bits. Classical examples of primitive types are integers and floating-point values. Unlike most languages, where only a fixed set of built-in primitive types is available, Julia allows you to define your own such types. In fact, all the standard primitive types are defined in the language itself.
primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end
primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end
primitive type Int8 <: Signed 8 end
primitive type UInt8 <: Unsigned 8 end
primitive type Int16 <: Signed 16 end
primitive type UInt16 <: Unsigned 16 end
primitive type Int32 <: Signed 32 end
primitive type UInt32 <: Unsigned 32 end
primitive type Int64 <: Signed 64 end
primitive type UInt64 <: Unsigned 64 end
primitive type Int128 <: Signed 128 end
primitive type UInt128 <: Unsigned 128 end
The standard syntactic constructions for declaring a primitive type look like this:
primitive type «name» «bits» end primitive type «name» <: «supertype» «bits» end
The bits element indicates how many bits are needed to store the type, and the name element defines the name of the new type. You can also declare a primitive type as a subtype of a supertype. If no supertype is specified, then the immediate default supertype is Any'. Thus, the above declaration `Bool
means that eight bits are needed to store a boolean value, and its immediate supertype is the type Integer
. Currently, only sizes that are multiples of 8 bits are supported, and LLVM errors are likely when using sizes other than those listed above. Therefore, although only one bit is sufficient to represent a boolean value, it cannot be declared to be less than 8 bits in size.
Types Bool
, Int8
and 'UInt8` have identical representations in memory: they all occupy 8 bits. However, since the Julia type system is nominative, despite the identical structure, these types are not interchangeable. The fundamental difference between them is that they have different supertypes: Bool
direct supertype — Integer
, y Int8
— Signed
, and y UInt8
— Unsigned
. Otherwise, the difference between Bool
, Int8
and 'UInt8` boils down to what behavior is defined for functions when passing objects of these types as arguments. That’s why a nominative type system is necessary: if the structure defined the type, and it, in turn, dictated behavior, it would be impossible to implement for Bool
different behavior than for Int8
or UInt8
.
Composite types
In different languages https://en.wikipedia.org/wiki/Composite_data_type [composite types] are called in different ways: records, structures, objects. A composite type is a collection of named fields, and you can work with its instance as a single value. In many languages, users can only define composite types, but in Julia, most user-defined types are composite.
In popular object-oriented languages such as C++ In Java, Python, and Ruby, named functions are also associated with composite types: collectively, this is called an object. In cleaner object-oriented languages such as Ruby or Smalltalk, objects are all values, whether composite or not. In less pure object-oriented languages, including C++ and Java, some values, such as integers and floating points, are not objects. True objects are instances of custom composite types with associated methods. In Julia, objects are any values, but functions are not bound to the objects they operate on. This is due to the fact that in Julia, the required function method is selected through multiple dispatching, which means that when choosing a method, the types of all arguments of the function are taken into account, and not just the first one (for more information about methods and dispatching, see the chapter Methods). Therefore, it would be wrong to depend on the function only on the first argument. Organizing methods as function objects instead of named sets of methods within each object turns out to be an extremely useful feature of the language design.
Compound types are declared using a keyword struct
, followed by a block of field names with optional type annotations via the ::
operator.
julia> struct Foo
bar
baz::Int
qux::Float64
end
Fields without type annotations are of type Any
by default and, accordingly, can contain any type of value.
New objects of type Foo
are created by applying type `Foo' as a function to field values.
julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)
julia> typeof(foo)
Foo
When a type is used as a function, it is called a constructor. Two constructors are created automatically (they are called constructors by default). One of them takes any arguments and calls the function convert
to convert them to field types, and the other takes arguments that exactly match the field types. Both of these constructors are created to make it easier to add new definitions without accidentally replacing the default constructor.
Since the type of the bar
field is unlimited, any value will do. However, the value of baz
must be such that it can be converted to Int
.
julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]
The list of field names can be obtained using the function fieldnames
.
julia> fieldnames(Foo)
(:bar, :baz, :qux)
The values of the fields of a composite object can be accessed using the generally accepted notation `foo.bar'.
julia> foo.bar
"Hello, world."
julia> foo.baz
23
julia> foo.qux
1.5
Composite objects declared using the keyword struct
are not change_: they cannot be changed after creation. It may seem strange at first, but this approach has several advantages.
-
It can be more effective. Some structures can be efficiently packed into arrays, and in some cases, the compiler can completely dispense with allocating memory for immutable objects.
-
It is impossible to violate the invariants provided by type constructors.
-
Code using immutable objects may be easier to comprehend.
The fields of an immutable object can contain mutable objects, such as arrays. Such nested objects remain mutable: you cannot change only the fields of the immutable object itself so that they point to other objects.
If necessary, you can declare mutable composite objects using a keyword. mutable struct
. This will be discussed in the next section.
If all fields of an immutable structure are indistinguishable from each other (===
), then two immutable values with such fields are also indistinguishable.
julia> struct X
a::Int
b::Float64
end
julia> X(1, 2) === X(1, 2)
true
Creating instances of composite types has a number of other important features, however, they relate to parametric types and methods, and therefore it makes sense to put their discussion in a separate chapter.: Constructors.
For many custom X
types, you may need to define a method Base.broadcastable(x::X) = Ref(x)
, so that instances of the type act as 0-dimensional "scalars" for the purposes of broadcasts.
Mutable composite types
If a composite type is declared using the keyword mutable struct
rather than struct
, then its instances can be modified.
julia> mutable struct Bar
baz
qux::Float64
end
julia> bar = Bar("Hello", 1.5);
julia> bar.qux = 2.0
2.0
julia> bar.baz = 1//2
1//2
An additional interface between the fields and the user can provide instance properties. They give you more control over which elements will be available for viewing and changing using the bar.baz
notation.
To support modification, such objects are usually placed on the heap and have fixed memory addresses. A mutable object is like a small container that can contain different values throughout its existence and is therefore reliably identified only by its address in memory. On the contrary, an instance of a mutable type is associated with certain field values that fully describe it. When deciding whether a type should be mutable, consider whether two instances with the same field values will be considered identical or whether they may change independently over time. If they are considered identical, most likely, the type should be immutable.
To summarize, immutability in Julia is characterized by two main qualities.
-
You cannot change the value of an immutable type.
-
For bit types, this means that the bit pattern of the value never changes after assignment and that the value allows identification of the bit type.
-
For composite types, this means that the identity of the field values remains unchanged. If the fields are of bit types, it means that their bits never change. If they belong to mutable types, such as arrays, this means that the field will always refer to the same mutable value, although the contents of this value may change.
-
-
An object of an immutable type can be freely copied by the compiler, since due to immutability, the source object and the copy are indistinguishable for the program.
-
In particular, this means that fairly small immutable values, such as integers and floating points, are usually passed to functions via registers (or placed on the stack).
-
In turn, mutable values are placed on the heap, and pointers to these values on the heap are passed to the function, except when the compiler knows that it is impossible to determine the opposite.
-
If it is known that only some fields of a mutable structure are immutable, these fields can be declared as such using the keyword const
, as shown below. This provides a number of optimizations for immutable structures and allows you to apply invariants to certain fields marked as `const'.
Compatibility: Julia 1.8
To apply the |
julia> mutable struct Baz
a::Int
const b::Float64
end
julia> baz = Baz(1, 1.5);
julia> baz.a = 2
2
julia> baz.b = 2.0
ERROR: setfield!: const field .b of type Baz cannot be changed
[...]
Declared types
The three categories of types that were discussed in the previous sections (abstract, primitive, and composite) are actually closely related to each other. They all have important properties in common.
-
They are declared explicitly.
-
They have names.
-
They have explicitly declared supertypes.
-
They may have parameters.
Because of these common properties, all these types are represented internally as instances of the DataType
— the type to which all these types belong.
julia> typeof(Real)
DataType
julia> typeof(Int)
DataType
The DataType' type can be abstract or concrete. If it is specific, it has a specific size, memory layout, and (optionally) field names. Thus, the primitive type is a non-zero-size `DataType
type, but without field names. A composite type is a DataType
type with field names or an empty one (zero size).
Each specific value in the system is an instance of some type of `DataType'.
Combining types
A type union is a special abstract type that includes all instances of all argument types as objects. It is created using a special keyword Union
.
julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64
Compilers of many languages provide an internal structure for combining, allowing you to define types; in Julia, it is available to programmers. The Julia compiler can generate efficient code when there are Union
unions that include a small number of footnote types.:1[The "small number" is determined by the max_union_splitting
configuration, which currently has a default value of 4.] To do this, specialized code is generated for each possible type in a separate branch.
A particularly useful application of the Union
type is the Union{T, Nothing}
, where T
can be any type, and Nothing'
is a single type, the only instance of which is an object. `nothing. Such a template in Julia is the equivalent of types https://en.wikipedia.org/wiki/Nullable_type [Nullable
, Option
, or Maybe
] in other languages. If you declare a function argument or field as a Union{T, Nothing}
, they can be assigned either a value like T
or nothing
, which means there is no value. For more information, see in this FAQ section.
Parametric types
An important and useful feature of the Julia type system is that it is parametric: types can accept parameters, so when declaring a type, a whole family of new types is introduced, one for each possible combination of parameter values. In many languages, it is supported in one form or another https://en.wikipedia.org/wiki/Generic_programming [universal programming], which allows you to define data structures and algorithms for working with them without specifying the exact types. For example, universal programming is implemented in one way or another in ML, Haskell, Ada, Eiffel, C++, Java, C#, F#, Scala and other languages. Some of these languages (e.g. ML, Haskell, Scala) support true parametric polymorphism, while others (e.g. C++ Java) — special styles of universal programming based on templates. Due to such a variety of approaches to universal programming and parametric types, we will not even try to compare Julia with other languages in this regard. Instead, we will focus on looking at this system in Julia proper. However, we note that many of the difficulties typical of static parametric type systems can be relatively easily overcome, since Julia is a dynamically typed language that does not require defining all types at compile time.
All declared types (varieties of `DataType') can be parameterized using the same syntax. We will consider them in the following order: first, parametric composite types, then parametric abstract types, and finally parametric primitive types.
Parametric composite types
The type parameters are specified immediately after its name in curly brackets.
julia> struct Point{T}
x::T
y::T
end
Thus, a new parametric type Point' is defined.{T}
containing two coordinates of type T'. The question may arise: what is `T'? This is exactly the essence of parametric types.: it can be any type at all (or a value of any bit type, although in this case it is obvious that it is a type). `+Point{Float64}+
is a specific type equivalent to the one that would be obtained if we replaced T
with in the definition of Point'. `Float64
. Thus, using a single definition, an unlimited number of types are actually declared: Point{Float64}
, Point{AbstractString}
, Point{Int64}
and so on. Each of them can now be used as a specific one.
julia> Point{Float64}
Point{Float64}
julia> Point{AbstractString}
Point{AbstractString}
Type of Point{Float64}
represents a point whose coordinates are 64-bit floating-point values, and the type is Point{AbstractString}
— a point whose coordinates are string objects (see the section Lines).
By itself, the type Point
is also a valid object of the type of which all instances of Point' are subtypes.{Float64}
, Point{AbstractString}
and so on.
julia> Point{Float64} <: Point
true
julia> Point{AbstractString} <: Point
true
Other types, of course, are not its subtypes.
julia> Float64 <: Point
false
julia> AbstractString <: Point
false
Specific types of Point
with different values of T
are never subtypes of each other.
julia> Point{Float64} <: Point{Int64}
false
julia> Point{Float64} <: Point{Real}
false
The last point is very important: despite the fact that the expression |
In other words, in terms of type theory, Julia’s type parameters are invariant, not https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29 [covariant (or contravariant)]. There is a practical explanation for this: although the instance of Point{Float64}
essentially similar to an instance of Point{Real}
, these two types have different representations in memory.
-
Instance of
Point{Float64}
can be compactly and efficiently represented as a pair of 64-bit values stored side by side. -
To the instance of
Point{Real}
any pair of instances of the type must be placedReal
. Since objects that are instances ofReal
can be of arbitrary size and structure, in practice the instance isPoint'.{Real}
must be represented by a pair of pointers to separateReal
objects in memory.
Efficiency advantages due to the storage of Point' values{Float64}
nearby in memory are much more pronounced in the case of arrays: Array{Float64}
can be stored in memory as a contiguous block of 64-bit floating-point values, while an instance of Array{Real}
must be an array of pointers to individual objects Real
in memory — it can be like https://en.wikipedia.org/wiki/Object_type_%28object-oriented_programming%29#Boxing [packaged] 64-bit floating-point values, as well as complex objects of arbitrary size declared as implementations of the abstract type `Real'.
Since Point{Float64}
is not a subtype of Point{Real}
, the following method cannot be applied to arguments of type Point{Float64}
.
function norm(p::Point{Real})
sqrt(p.x^2 + p.y^2)
end
The correct way to define a method that accepts all arguments of type Point{T}
, where T
is a subtype Real
, looks like this:
function norm(p::Point{<:Real})
sqrt(p.x^2 + p.y^2)
end
(Similarly, one could define function norm(p::Point{T} where T<:Real)
or function norm(p::Point{T}) where T<:Real
; see the section unionAll types.)
Additional examples will be given later in the chapter. Methods.
How do I create a Point
object? You can define custom constructors for composite types; this will be discussed in detail in the chapter Constructors. In the absence of special constructor declarations, there are two standard ways to create composite objects: with explicit type parameters and with their output based on the arguments passed to the object constructor.
Since Point{Float64}
is a specific type, equivalent to the type Point
, declared with the indication Float64
instead of T
, it can be used as a constructor.
julia> p = Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(p)
Point{Float64}
When using the default constructor, exactly one argument must be passed 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.
[...]
For parametric types, only one default constructor is created, since it cannot be overridden. This constructor accepts any arguments and converts them to field types.
It is often unnecessary to specify the type of the Point
object to be created, since it is implicitly determined based on the types of arguments passed in the constructor call. For this reason, you can use the Point
type itself as a constructor, provided that the implied value of the T
parameter type 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 of Point
, the implied type T
is unambiguous if and only if both arguments to Point
have the same type. If this is not the case, the constructor will throw an exception. MethodError
.
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:2
Stacktrace:
[...]
To properly handle such mixed cases, you can define special constructor methods, but this will be discussed later in the chapter. Constructors.
Parametric abstract types
When declaring a parametric abstract type, a collection of abstract types is declared in much the same way.
julia> abstract type Pointy{T} end
With this declaration, Pointy{T}
represents a separate abstract type for each type or integer value of `T'. Just as in the case of parametric composite types, each such instance is a subtype of `Pointy'.
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{1} <: Pointy
true
Parametric abstract types, like composite types, are invariant.
julia> Pointy{Float64} <: Pointy{Real}
false
julia> Pointy{Real} <: Pointy{Float64}
false
The Pointy' notation{<:Real}
allows you to express an analog of a variant type in Julia, and the notation Pointy{>:Int}
— is an analog of the contravariant type, but formally they represent sets of types (see the section unionAll types).
julia> Pointy{Float64} <: Pointy{<:Real}
true
julia> Pointy{Real} <: Pointy{>:Int}
true
Just as regular abstract types serve to form a type hierarchy into which concrete types fit, parametric abstract types serve the same useful purpose, but with respect to parametric composite types. For example, we could declare Point{T}+`as a subtype'+Pointy{T}
as follows.
julia> struct Point{T} <: Pointy{T}
x::T
y::T
end
In the case of such an ad, Point{T}
is a subtype of Pointy{T}
for each value of T
.
julia> Point{Float64} <: Pointy{Float64}
true
julia> Point{Real} <: Pointy{Real}
true
julia> Point{AbstractString} <: Pointy{AbstractString}
true
The relation is also invariant.
julia> Point{Float64} <: Pointy{Real}
false
julia> Point{Float64} <: Pointy{<:Real}
true
What are parametric abstract types like Pointy
for? Let’s say we have created an implementation of a point object that requires only one coordinate, because the point is located on the diagonal line x = y.
julia> struct DiagPoint{T} <: Pointy{T}
x::T
end
Now and Point{Float64}
, and DiagPoint{Float64}
are implementations of abstraction Pointy{Float64}
. The same is true for other possible variants of the type T'. This allows you to program a common interface for all `Pointy
objects, implemented for both Point
and `DiagPoint'. However, it is not yet possible to fully demonstrate this concept, since methods and dispatching will be discussed only in the next chapter. Methods.
In some situations, type parameters should not take all possible types as values, since some of them do not make sense. In this case, the set of possible values of T
can be limited as follows.
julia> abstract type Pointy{T<:Real} end
With such a declaration, any type that is a subtype can be used instead of T
. Real
, but not other types that are subtypes of Real
.
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
The parameters of parametric composite types can be limited in the same way.
struct Point{T<:Real} <: Pointy{T}
x::T
y::T
end
As an example of the useful use of parametric types, here is a real definition of an immutable type. Rational
in Julia, representing the exact value of an integer fraction (omitted the constructor for simplicity).
struct Rational{T<:Integer} <: Real
num::T
den::T
end
Types of tuples
A tuple is an abstraction of function arguments without the function itself. The essential aspects of function arguments are their order and types. Thus, the tuple type is similar to a parameterized immutable type, each parameter of which is the type of a single field. For example, the type of a tuple of two elements is similar to the following immutable type.
struct Tuple2{A,B}
a::A
b::B
end
However, there are three main differences.
-
Tuple types can have any number of parameters.
-
Tuple types are variant in their parameters:
Tuple{Int}
is a subtype ofTuple{Any}
. Therefore,Tuple{Any}
is considered an abstract type, and tuple types are concrete only if their parameters are such. -
Tuples do not have field names; fields are only accessible by indexes.
The tuple values are written in parentheses separated by commas. When creating a tuple, the corresponding tuple type is generated on demand.
julia> typeof((1,"foo",2.5))
Tuple{Int64, String, Float64}
Pay attention to the consequences of covariance.
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true
julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false
julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false
On an intuitive level, this is equivalent to saying that the type of arguments to a function is a subtype of its signature if it matches.
Types of tuples with a variable number of arguments
The last parameter of the tuple type can be a special value. Vararg
, which means any number of elements at the end.
julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString, Vararg{Int64}}
julia> isa(("1",), mytupletype)
true
julia> isa(("1",1), mytupletype)
true
julia> isa(("1",1,2), mytupletype)
true
julia> isa(("1",1,2,3.0), mytupletype)
false
Moreover, the expression Vararg{T}
corresponds to zero elements of type T
or more. Tuple types with the Vararg parameter are used to represent arguments accepted by methods with a variable number of arguments (see the section Functions with a variable number of arguments (Vararg)).
The special value of Vararg{T,N}
(used as the last tuple type parameter) corresponds to exactly N
elements of type T'. `NTuple{N,T}
— this is a convenient alias for Tuple{Vararg{T,N}}
, that is, the type of tuple containing exactly N
elements of type `T'.
Named tuple types
Named tuples are instances of the type NamedTuple
, which has two parameters: a tuple of characters with field names and a tuple type with field types. For convenience, the NamedTuple
types are output using a macro @NamedTuple
, which provides a more convenient structure-style syntax (struct
) for declaring these types via key::Type
declarations, where the missing ::Type
corresponds to ::Any
.
julia> typeof((a=1,b="hello")) # выводит в виде макроса
@NamedTuple{a::Int64, b::String}
julia> NamedTuple{(:a, :b), Tuple{Int64, String}} # длинная форма типа
@NamedTuple{a::Int64, b::String}
The begin ... end
form of the @NamedTuple
macro allows you to split declarations into several lines (just as in the case of the struct declaration). But otherwise it is equivalent to the following:
julia> @NamedTuple begin
a::Int
b::String
end
@NamedTuple{a::Int64, b::String}
The NamedTuple' type can be used as a constructor that accepts a single argument as a tuple. The `NamedTuple
type being created can be either a specific type if both parameters are specified, or a type with only field names.
julia> @NamedTuple{a::Float32,b::String}((1, ""))
(a = 1.0f0, b = "")
julia> NamedTuple{(:a, :b)}((1, ""))
(a = 1, b = "")
If the field types are specified, the arguments are converted. Otherwise, the argument types are used directly.
Parametric primitive types
Primitive types can also be declared parametrically. For example, pointers are represented as primitive types that could be declared in Julia as follows.
# 32-разрядная система:
primitive type Ptr{T} 32 end
# 64-разрядная система:
primitive type Ptr{T} 64 end
A small oddity of these declarations in comparison with the usual parametric composite types is that the type parameter T
is not used in the definition of the type itself - it is just an abstract label defining an entire family of types with identical structure, which differ only in the type parameter. Thus, Ptr{Float64}
and`Ptr{Int64}` are separate types, although they have the same representations. And, of course, all the individual pointer types are subtypes of a common type. Ptr
.
julia> Ptr{Float64} <: Ptr
true
julia> Ptr{Int64} <: Ptr
true
Types of unionAll
We have already noted that a parametric type, such as Ptr
, acts as a supertype for all its instances (Ptr{Int64}
, etc.). How does it work? By itself, the Ptr
type cannot be an ordinary data type: it cannot be used for memory operations if the type of the corresponding data is unknown. The explanation is that Ptr
(or other parametric types, such as Array
) is a special kind of type called UnionAll
. This type means an iterable combination of types for all values of a certain parameter.
The types of unionAll' are usually written using the keyword `where'. For example, `Ptr
could be more accurately written as Ptr{T} where T
, which corresponds to all values of type Ptr{T}
for some value of T
. In this context, the parameter T' is also often referred to as a type variable, since it acts as a variable whose values can be a specific set of types. Each `where
keyword introduces a single type variable, so such expressions can be nested for types with multiple parameters, such as Array{T,N} where N where T
.
Syntax for using type A{B,C}`requires `A
to be of type unionAll', and first `B
is substituted into an external type variable in A
. The result should be a different type of unionAll
, which is then substituted with C
. Thus, A{B,C}
equivalent to A{B}{C}
. This explains why partial instantiation of a type such as Array' is possible.{Float64}
: the value of the first parameter is fixed, but the second one still accepts all possible values. When using the where
syntax Any subset of parameters can be explicitly fixed. For example, the type of all one-dimensional arrays can be written as Array{T,1} where T
.
Type variables can be limited through subtype relationships. Array{T} where T<:Integer
means all arrays with element types that are some subtypes. Integer
. For the syntactic construction Array{T} where T<:Integer
has a convenient short form of writing: Array{<:Integer}
. Variables of types can have lower and upper bounds. Array{T} where Int<:T<:Number
means all arrays of elements Number
, which can contain Int
(the size of T
must be at least Int
). Using the where T> syntax:Int
can also specify only the lower bound of a type variable, and Array'{>:Int}`equivalent to `+Array{T} where T>:Int+
.
Since where
expressions can be nested, the boundaries of a type variable can relate to external type variables. For example, Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real
means two-element tuples, the first element of which belongs to a certain subtype Real
, and the second element is any array whose elements are of the type of the first element of the tuple.
The where
keyword itself can be embedded in more complex ads. For example, consider two types created through 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 T
Array{Vector{T}, 1} where T
The type T1
defines a one-dimensional array of one-dimensional arrays; each internal array consists of objects of the same type, but these types of objects in the internal arrays may be different. In turn, the type T2
defines a one-dimensional array of one-dimensional arrays in which the objects of all internal arrays belong to the same type. Note that T2
is an abstract type (for example, Array{Array{Int,1},1} <: T2
), and T1
is specific. For this reason, an instance of T1
can be created using the constructor without arguments (a=T1()
), but an instance of T2
cannot.
There is a convenient syntax for naming such types, similar to the short form of defining functions.
Vector{T} = Array{T, 1}
This is equivalent to const Vector = Array{T,1} where T
. Record Vector{Float64}
is equivalent to Array{Float64,1}
, and instances of the general type Vector
are all objects of Array
, in which the second parameter - the number of dimensions of the array - is equal to 1, regardless of the type of elements. In languages where parametric types must always be specified in full, this is not particularly useful, but in Julia, thanks to this, you can simply write a Vector
to define an abstract type that includes all one-dimensional dense arrays with elements of any type.
Single types
Immutable composite types without fields are called single types. Formally speaking, if
-
'T` is an immutable composite type (defined using the keyword `struct') and
-
from
a isa T && b isa T
followsa === b
,
the T
is a single type.[1] To check whether the type is single, use the function Base.issingletontype
. Abstract types cannot be singular by their nature.
By definition, a single type can have only one instance.
julia> struct NoFields
end
julia> NoFields() === NoFields()
true
julia> Base.issingletontype(NoFields)
true
Function '=== confirms that the created instances of `NoFields are identical to each other.
Parametric types can be single if the above condition is met. Examples:
julia> struct NoFieldsParam{T}
end
julia> Base.issingletontype(NoFieldsParam) # Не может быть одинарным типом...
false
julia> NoFieldsParam{Int}() isa NoFieldsParam # ...так как имеет...
true
julia> NoFieldsParam{Bool}() isa NoFieldsParam # ...несколько экземпляров.
true
julia> Base.issingletontype(NoFieldsParam{Int}) # В параметризованной форме является одинарным.
true
julia> NoFieldsParam{Int}() === NoFieldsParam{Int}()
true
Types of functions
Each function has its own type, which is a subtype of `Function'.
julia> foo41(x) = x + 1
foo41 (generic function with 1 method)
julia> typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)
Note that the output of the call is `typeof(foo41)`similar to the challenge itself. This is just a convention, since this object is completely independent and can be used as any other value.
julia> T = typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)
julia> T <: Function
true
The types of functions defined at the top level are single. If necessary, they can be compared using the operator ===
.
At closures also have their own types, whose names are usually output with #<number>
at the end. Functions defined in different places have unique names and types, and the output may vary from session to session.
julia> typeof(x -> x + 1)
var"#9#10"
Closure types are not necessarily single.
julia> addy(y) = x -> x + y
addy (generic function with 1 method)
julia> typeof(addy(1)) === typeof(addy(2))
true
julia> addy(1) === addy(2)
false
julia> Base.issingletontype(typeof(addy(1)))
false
Type selectors Type{T}
For each type T
Type{T}
is an abstract parametric type, the only instance of which is the object T
. They haven’t been reviewed yet parametric methods and transformations, the purpose of this construction is difficult to explain, but, in short, it allows you to specialize the behavior of a function for certain types as _values. This is useful for writing methods (especially parametric ones) whose behavior depends on the type passed explicitly as an argument, rather than being derived from the type of one of the arguments.
Since this definition can be a bit difficult to understand, let’s look at some examples.
julia> isa(Float64, Type{Float64})
true
julia> isa(Real, Type{Float64})
false
julia> isa(Real, Type{Real})
true
julia> isa(Float64, Type{Real})
false
In other words, isa(A, Type{B})
is true if and only if A
and B
are the same object and this object is a type.
In particular, since parametric types invariant:
julia> struct TypeParamExample{T}
x::T
end
julia> TypeParamExample isa Type{TypeParamExample}
true
julia> TypeParamExample{Int} isa Type{TypeParamExample}
false
julia> TypeParamExample{Int} isa Type{TypeParamExample{Int}}
true
Without the 'Type` parameter, it is just an abstract type, of which all objects of types are instances.
julia> isa(Type{Float64}, Type)
true
julia> isa(Float64, Type)
true
julia> isa(Real, Type)
true
Any object other than a type is not an instance of Type
.
julia> isa(1, Type)
false
julia> isa("foo", Type)
false
Although Type
is part of the Julia type hierarchy, like any other abstract parametric type, it is usually not used outside of method signatures, except in a number of special cases. Another important use of Type
is to specify field types that would otherwise be less accurate, for example DataType
in the example below. In this case, the default constructor could lead to performance problems for code that uses the exact type enclosed in the shell (similarly parameters of abstract types).
julia> struct WrapType{T}
value::T
end
julia> WrapType(Float64) # Конструктор по умолчанию, обратите внимание на DataType
WrapType{DataType}(Float64)
julia> WrapType(::Type{T}) where T = WrapType{Type{T}}(T)
WrapType
julia> WrapType(Float64) # Специализированный конструктор, обратите внимание на более точный тип Type{Float64}
WrapType{Type{Float64}}(Float64)
Type aliases
Sometimes it is convenient to enter a new name for an already expressible type. This can be done using a simple assignment operator. For example, an alias for UInt
can be UInt32
or UInt64
depending on the size of the pointers in the system.
# 32-разрядная система:
julia> UInt
UInt32
# 64-разрядная система:
julia> UInt
UInt64
For this purpose, in base/boot.jl
uses the following code.
if Int === Int64
const UInt = UInt64
else
const UInt = UInt32
end
Of course, it depends on which type of Int
the alias is created for, but it must be the correct type: either Int32
, or Int64
.
(Note that, unlike Int
, Float
is not an alias for the type. AbstractFloat
of a certain size. Unlike integer registers, where the size of an Int
corresponds to the size of a machine pointer in the system, the sizes of floating-point registers are defined in the IEEE-754 standard.)
Type aliases can be parameterized:
julia> const Family{T} = Set{T}
Set
julia> Family{Char} === Set{Char}
true
Operations with types
Since types in Julia are objects themselves, ordinary functions can operate on them. You have already familiarized yourself with some functions designed to work with types or analyze them. These include the operator <:
, indicating that the left operand is a subtype of the right one.
Function 'isa` checks whether an object belongs to a certain type and returns true or false.
julia> isa(1, Int)
true
julia> isa(1, AbstractFloat)
false
Function typeof
, which has already been used in the examples in this guide, returns the type of its argument. Since, as mentioned earlier, types are objects, they themselves have types and can be recognized.
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,String})
Union
But what if we repeat this operation? What type does the type type belong to? It turns out that all types are composite values and thus have the type `DataType'.
julia> typeof(DataType)
DataType
julia> typeof(Union)
DataType
The `DataType' is a type for itself.
Another operation applicable to some types is supertype
. It defines the type’s supertype. Only declared types (DataType
) have uniquely defined supertypes.
julia> supertype(Float64)
AbstractFloat
julia> supertype(Number)
Any
julia> supertype(AbstractString)
Any
julia> supertype(Any)
Any
When applying the function supertype
an exception is raised for other objects of types (or for objects that are not types). MethodError
.
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:
[...]
Customizable structural printout
Sometimes you need to configure the display of type instances. To do this, overload the function show
. For example, let’s assume that we have defined a type for representing complex numbers in polar form.
julia> struct Polar{T<:Real} <: Number
r::T
Θ::T
end
julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar
Here we have added a custom constructor function that can accept arguments of different types. Real
and promote them to a general type (see chapters Constructors and Transformation and promotion). (Of course, we would also have to define many other methods so that the type could be used as Number
, for example +
, *
, ` one`, zero
, promotion rules, etc.) By default, instances of this type are displayed quite simply: the type name and field values are displayed, for example Polar{Float64}(3.0,4.0)
.
If we wanted information to be output as 3.0 * exp(4.0im)
instead, we would define the following method to output information to the specified output object `io' (representing a file, terminal, buffer, etc.; see the section Network and Streaming).
julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")
More detailed control over the display of Polar objects is also possible. In particular, sometimes you need both a detailed multi-line format for displaying information about a single object in REPL and other interactive environments, and a shorter one-line format for a function. print
or to display an object as part of another object (for example, an array). Although the show(io, z)
function is called by default in both cases, you can define another multiline format for displaying an object using an overload of the show
function with three arguments, where the second argument is the MIME type `text/plain' (see section Multimedia data input/output).
julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
print(io, "Polar{$T} complex number:\n ", z)
(Note that by using print(..., z)
, the show(io, z)
method is called here with two arguments.) The result will be as follows:
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)
Here, the one-line option show(io, z)
is still used for the array of Polar
values. Strictly speaking, the REPL environment calls display(z)
to display the result of string execution, which by default corresponds to the call show(stdout, MIME("text/plain"), z)
, and it, in turn, corresponds to the call show(stdout, z)'. However, you should not define new methods. `display
, unless you define a new handler for displaying multimedia data (see section Multimedia data input/output).
Moreover, you can also define show
methods for other MIME types to ensure that information about objects with more complex formatting (HTML markup, images, etc.) is displayed in environments that support this feature (for example, IJulia). For example, you can define the display of information about Polar
objects in HTML format with superscripts and italics as follows:
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>")
As a result, in environments that support HTML, information about the Polar
object will be automatically displayed using HTML, however, if desired, you can manually call the show
function to get the output data in HTML format.
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.0 e^4.0 i^
As a general rule, the one-line show' method should output a valid Julia expression to create the displayed object. If this `show
method contains infix operators, for example the multiplication operator (*
) in the one-line show
method for the Polar
type above, it may be incorrectly analyzed when output as part of another object. To illustrate this, let’s take an expression object (see section Program Representation), which squares a specific instance of the type `Polar'.
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
Since the operator ^
takes precedence over *
( See the section Operator precedence and associativity), this output incorrectly represents the expression a^2
, which should correspond to the expression (3.0*exp(4.0im)) ^ 2
. To solve this problem, you need to create a custom method for Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int)
, which will be automatically called by the expression object when information is output.
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
end
julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)
The method defined above encloses the show
call in parentheses when the priority of the calling operator is higher than or equal to the multiplication priority. This check allows you to omit parentheses when outputting an expression that is correctly parsed without them (for example, :($a + 2)
and :($a == 2)
).
julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)
julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)
In some cases, it may be useful to customize the behavior of the show
methods depending on the context. To do this, you can use the type 'IOContext`, which allows passing contextual properties along with the wrapped I/O stream. For example, the show' method can produce a more concise representation when the `:compact
property has the value true
, or a complete representation when this property has the value false
or is not set.
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 short representation will be used when the input/output stream being transmitted is an IOContext
object with the specified :compact
property. In particular, this will happen when outputting arrays with multiple columns (when the width is limited).
julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im
julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Matrix{Polar{Float64}}:
3.0ℯ4.0im 4.0ℯ5.3im
In the documentation for 'IOContext` provides a list of frequently used properties that can be applied to customize the output.
Types of values
In Julia, dispatching by _value, such as true
or false
, is not possible. However, parametric type dispatching is possible, and Julia allows you to include simple bit values (types, characters, integers, floating-point numbers, tuples, etc.) as type parameters. A common example is the dimension parameter in an Array{T,N}
, where T
is the type (for example, Float64
), but N
is just an `Int'.
You can create your own custom types that take values as parameters and use them to manage the dispatching of custom types. To illustrate this concept, we introduce the parametric type Val{x}
and its constructor Val(x) = Val{x}()
, which allows you to use this technique in cases where a more developed hierarchy is not required.
Val
is defined as follows:
julia> struct Val{x}
end
julia> Val(x) = Val{x}()
Val
The implementation of Val
is limited to this. Some functions in the Julia standard library accept instances of Val
as arguments, and you can use this type 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 purposes, in Julia, the example Val
should always be passed at the call location, not the type, that is, foo(Val(:bar))
should be used, not foo(Val{:bar})
.
It is worth noting that parametric value types, including Val
, are very easy to use incorrectly and, in the worst case, code performance can significantly decrease. In particular, you should never write code as shown above. For more information about appropriate (and inappropriate) applications of Val
, see performance tips.