Types
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 in Methods, 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 programs without ever explicitly using types. When additional expressiveness is needed, however, it is easy to gradually introduce explicit type annotations into previously “untyped” code. Doing so will typically increase both the performance and robustness of these systems, and perhaps somewhat counterintuitively, often significantly simplify them.
Describing Julia in the lingo of type systems, it is: dynamic, nominative, parametric and dependent. Generic types can be parameterized by other types and by integers, and the hierarchical relationships between types are explicitly declared, rather than implied 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:
- There is no division between object and non-object values: all values in Julia are true objects having a type that belongs to a single, fully connected type graph, all nodes of which are equally first-class as types.
- There is no meaningful concept of a “compile-time type”: the only type a value has is its actual type when the program is running. This is called a “run-time type” in object-oriented languages where the combination of static compilation with polymorphism makes this distinction significant.
- Only values, not variables, have types — variables are simply names bound to values.
- Both abstract and concrete types can be paramaterized by other types and by integers. Type parameters may be completely omitted when they do not need to be explicitly referenced or restricted.
Type Declarations
The :: operator can be used to attach type annotations to expressions and variables in programs. There are two primary reasons to do this:- As an assertion to help confirm that your program works the way you expect,
- To provide extra type information to the compiler, which can then improve performance in many cases
julia> (1+2)::Float
type error: typeassert: expected Float, got Int64
julia> (1+2)::Int
3
When attached to a variable, 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 using the convert function:
julia> function foo()
x::Int8 = 1000
x
end
julia> foo()
-24
julia> typeof(ans)
Int8
The “declaration” behavior only occurs in specific contexts:
x::Int8 # a variable by itself
local x::Int8 # in a local declaration
x::Int8 = 10 # as the left-hand side of an assignment
Abstract Types
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 in Integers and Floating-Point Numbers, we introduced a variety of concrete types of numeric values: Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Float32, and Float64. These are all bits types, which we will discuss in the next section. Although they have different representation sizes, Int8, Int16, Int32 and Int64 all have in common that they are signed integer types. Likewise Uint8, Uint16, Uint32 and Uint64 are all unsigned integer types, while Float32 and Float64 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 particular kind of integer, as long as the appropriate low-level implementations of integer operations are used. 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 the abstract keyword. The general syntaxes for declaring an abstract type are:
abstract «name»
abstract «name» <: «supertype»
When no supertype is given, the default supertype is Any — 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 called None. It is the exact opposite of Any: no object is an instance of None and all types are supertypes of None.
As a specific example, let’s consider a subset of the abstract types that make up Julia’s numerical hierarchy:
abstract Number
abstract Real <: Number
abstract Float <: Real
abstract Integer <: Real
abstract Signed <: Integer
abstract Unsigned <: Integer
The <: operator in general means “is a subtype of”, and, used in declarations like this, 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 returns true when its left operand is a subtype of its right operand:
julia> Integer <: Number
true
julia> Integer <: Float
false
Bits Types
A bits type is a concrete type whose data consists of plain old bits. Classic examples of bits types are integers and floating-point values. Unlike most languages, Julia lets you declare your own bits types, rather than providing only a fixed set of built-in bits types. In fact, the standard bits types are all defined in the language itself:bitstype 32 Float32 <: Float
bitstype 64 Float64 <: Float
bitstype 8 Bool <: Integer
bitstype 32 Char <: Integer
bitstype 8 Int8 <: Signed
bitstype 8 Uint8 <: Unsigned
bitstype 16 Int16 <: Signed
bitstype 16 Uint16 <: Unsigned
bitstype 32 Int32 <: Signed
bitstype 32 Uint32 <: Unsigned
bitstype 64 Int64 <: Signed
bitstype 64 Uint64 <: Unsigned
bitstype «bits» «name»
bitstype «bits» «name» <: «supertype»
The types Bool, Int8 and Uint8 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. Another fundamental difference between them is that they have different supertypes: Bool‘s direct supertype is Integer, Int8‘s is Signed, and Uint8‘s is Unsigned. All other differences between Bool, Int8, and Uint8 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, it would be impossible to make Bool behave any differently than Int8 or Uint8.
Composite Types
Composite types are called records, structures (“structs” in C), 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 Python and Ruby, 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, as in Python and Ruby, 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 of all of a function’s arguments are considered when selecting a method, rather than just the first one (see Methods for more information on methods and dispatch). Thus, it would be inappropriate for functions to “belong” to only their first argument. Organizing methods by association with function objects rather than simply having named bags of methods “inside” each object ends up being a highly beneficial aspect of the language design.Since composite types are the most common form of user-defined concrete type, they are simply introduced with the type keyword followed by a block of field names, optionally annotated with types using the :: operator:
type Foo
bar
baz::Int
qux::Float64
end
New objects of composite type Foo are created by applying the Foo 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
julia> Foo((), 23.5, 1)
no method Foo((),Float64,Int64)
julia> foo.bar
"Hello, world."
julia> foo.baz
23
julia> foo.qux
1.5
julia> foo.qux = 2
2.0
julia> foo.bar = 1//2
1//2
type NoFields
end
julia> is(NoFields(), NoFields())
true
There is much more to say about how instances of composite types are created, but that discussion depends on both Parametric Types and on Methods, and is sufficiently important to be addressed in its own section: Constructors.
Type Unions
A type union is a special abstract type which includes as objects all instances of any of its argument types, constructed using the special Union function:julia> IntOrString = Union(Int,String)
Union(Int,String)
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
type error: typeassert: expected Union(Int,String), got Float64
julia> Union()
None
Tuple Types
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. The type of a tuple of values is the tuple of types of values:julia> typeof((1,"foo",2.5))
(Int64,ASCIIString,Float64)
julia> (1,"foo",2.5) :: (Int64,String,Any)
(1,"foo",2.5)
julia> (1,"foo",2.5) :: (Int64,String,Float32)
type error: typeassert: expected (Int64,String,Float32), got (Int64,ASCIIString,Float64)
julia> (1,"foo",2.5) :: (Int64,String,3)
type error: typeassert: expected Type{T}, got (BitsKind,AbstractKind,Int64)
julia> typeof(())
()
Parametric Types
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 of generic 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.The only kinds of types that are declared are abstract types, bits types, and composite types. All such types can be parameterized, with the same syntax in each case. We will discuss them in in the following order: first, parametric composite types, then parametric abstract types, and finally parametric bits types.
Parametric Composite Types
Type parameters are introduced immediately after the type name, surrounded by curly braces:type Point{T}
x::T
y::T
end
julia> Point{Float64}
Point{Float64}
julia> Point{String}
Point{String}
julia> Point
Point{T}
julia> Point{Float64} <: Point
true
julia> Point{String} <: Point
true
julia> Float64 <: Point
false
julia> String <: Point
false
julia> Point{Float64} <: Point{Int64}
false
julia> Point{Float64} <: Point{Real}
false
In other words, in the parlance of type theory, Julia’s type parameters are invariant, rather than being covariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:Even though ``Float64 <: Real`` we DO NOT have ``Point{Float64} <: Point{Real}``.
- An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
- An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
How does one construct a Point object? It is possible to define custom constructors for composite types, which will be discussed in detail in Constructors, 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 type Point{Float64} is a concrete type equivalent to Point declared with Float64 in place of T, it can be applied as a constructor accordingly:
julia> Point{Float64}(1.0,2.0)
Point(1.0,2.0)
julia> typeof(ans)
Point{Float64}
julia> Point{Float64}(1.0)
no method Point(Float64,)
julia> Point{Float64}(1.0,2.0,3.0)
no method Point(Float64,Float64,Float64)
In many cases, it is redundant to provide the type of Point 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 apply Point itself as a constructor, provided that the implied value of the parameter type T is unambiguous:
julia> Point(1.0,2.0)
Point(1.0,2.0)
julia> typeof(ans)
Point{Float64}
julia> Point(1,2)
Point(1,2)
julia> typeof(ans)
Point{Int64}
julia> Point(1,2.5)
no method Point(Int64,Float64)
Parametric Abstract Types
Parametric abstract type declarations declare a collection of abstract types, in much the same way:abstract Pointy{T}
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{1} <: Pointy
true
julia> Pointy{Float64} <: Pointy{Real}
false
julia> Pointy{Real} <: Pointy{Float64}
false
type Point{T} <: Pointy{T}
x::T
y::T
end
julia> Point{Float64} <: Pointy{Float64}
true
julia> Point{Real} <: Pointy{Real}
true
julia> Point{String} <: Pointy{String}
true
julia> Point{Float64} <: Pointy{Real}
false
type DiagPoint{T} <: Pointy{T}
x::T
end
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 of T like so:
abstract Pointy{T<:Real}
julia> Pointy{Float64}
Pointy{Float64}
julia> Pointy{Real}
Pointy{Real}
julia> Pointy{String}
type error: Pointy: in T, expected Real, got AbstractKind
julia> Pointy{1}
type error: Pointy: in T, expected Real, got Int64
type Point{T<:Real} <: Pointy{T}
x::T
y::T
end
type Rational{T<:Integer} <: Real
num::T
den::T
end
Singleton Types
There is a special kind of abstract parametric type that must be mentioned here: singleton types. For each type, T, the “singleton type” Type{T} is an abstract type whose only instance is the object T. Since the definition is a little difficult to parse, 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
julia> isa(Type{Float64},Type)
true
julia> isa(Float64,Type)
true
julia> isa(Real,Type)
true
julia> isa(1,Type)
false
julia> isa("foo",Type)
false
A few popular languages have singleton types, including Haskell, Scala and Ruby. In general usage, the term “singleton type” refers to a type whose only instance is a single value. This meaning applies to Julia’s singleton types, but with that caveat that only type objects have singleton types, whereas in most languages with singleton types, every object has one.
Parametric Bits Types
Bits types can also be declared parametrically. For example, pointers are represented as boxed bits types which would be declared in Julia like this:# 32-bit system:
bitstype 32 Ptr{T}
# 64-bit system:
bitstype 64 Ptr{T}
julia> Ptr{Float64} <: Ptr
true
julia> Ptr{Int64} <: Ptr
true
Type Aliases
Sometimes it is convenient to introduce a new name for an already expressible type. For such occasions, Julia provides the typealias mechanism. For example, Uint is type aliased to either Uint32 or Uint64 as is appropriate for the size of pointers on the system:# 32-bit system:
julia> Uint
Uint32
# 64-bit system:
julia> Uint
Uint64
if is(Int,Int64)
typealias Uint Uint64
else
typealias Uint Uint32
end
For parametric types, typealias can be convenient for providing a new parametric types name where one of the parameter choices is fixed. Julia’s arrays have type Array{T,n} where T is the element type and n is the number of array dimensions. For convenience, writing Array{Float64} allows one to specify the element type without specifying the dimension:
julia> Array{Float64,1} <: Array{Float64} <: Array
true
typealias Vector{T} Array{T,1}
typealias Matrix{T} Array{T,2}
Operations on Types
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.The isa function tests if an object is of a given type and returns true or false:
julia> isa(1,Int)
true
julia> isa(1,Float)
false
julia> typeof(Real)
AbstractKind
julia> typeof(Float64)
BitsKind
julia> typeof(Rational)
CompositeKind
julia> typeof(Union(Real,Float64,Rational))
UnionKind
julia> typeof((Real,Float64,Rational,None))
(AbstractKind,BitsKind,CompositeKind,UnionKind)
- Abstract types have type AbstractKind
- Bits types have type BitsKind
- Composite types have type CompositeKind
- Unions have type UnionKind
- Tuples of types have a type that is the tuple of their respective kinds.
julia> typeof(AbstractKind)
CompositeKind
julia> typeof(BitsKind)
CompositeKind
julia> typeof(CompositeKind)
CompositeKind
julia> typeof(UnionKind)
CompositeKind
julia> typeof(())
()
julia> typeof(CompositeKind)
CompositeKind
julia> typeof(((),))
((),)
julia> typeof((CompositeKind,))
(CompositeKind,)
julia> typeof(((),CompositeKind))
((),CompositeKind)
Another operation that applies to some kinds of types is super. Only abstract types (AbstractKind), bits types (BitsKind), and composite types (CompositeKind) have a supertype, so these are the only kinds of types that the super function applies to:
julia> super(Float64)
Float
julia> super(Number)
Any
julia> super(String)
Any
julia> super(Any)
Any
julia> super(Union(Float64,Int64))
no method super(UnionKind,)
julia> super(None)
no method super(UnionKind,)
julia> super((Float64,Int64))
no method super((BitsKind,BitsKind),)
No comments:
Post a Comment
Thank you