Skip to content

Commit 2db8d2d

Browse files
committed
docs: Clarify important points about parametric types
Closes JuliaLang#43811. Removed some of the jargon and wikipedia links to make room for a brief alternative `Point{T1,T2}` demonstration. Shifted some paragraphs around to reflect their importance, and changed the wording in some places.
1 parent 522e568 commit 2db8d2d

File tree

1 file changed

+59
-54
lines changed

1 file changed

+59
-54
lines changed

doc/src/manual/types.md

+59-54
Original file line numberDiff line numberDiff line change
@@ -539,26 +539,34 @@ See [this FAQ entry](@ref faq-nothing) for more information.
539539

540540
An important and powerful feature of Julia's type system is that it is parametric: types can take
541541
parameters, so that type declarations actually introduce a whole family of new types -- one for
542-
each possible combination of parameter values. There are many languages that support some version
543-
of [generic programming](https://en.wikipedia.org/wiki/Generic_programming), wherein data structures
544-
and algorithms to manipulate them may be specified without specifying the exact types involved.
545-
For example, some form of generic programming exists in ML, Haskell, Ada, Eiffel, C++, Java, C#,
546-
F#, and Scala, just to name a few. Some of these languages support true parametric polymorphism
547-
(e.g. ML, Haskell, Scala), while others support ad-hoc, template-based styles of generic programming
548-
(e.g. C++, Java). With so many different varieties of generic programming and parametric types
549-
in various languages, we won't even attempt to compare Julia's parametric types to other languages,
550-
but will instead focus on explaining Julia's system in its own right. We will note, however, that
551-
because Julia is a dynamically typed language and doesn't need to make all type decisions at compile
552-
time, many traditional difficulties encountered in static parametric type systems can be relatively
553-
easily handled.
554-
555-
All declared types (the `DataType` variety) can be parameterized, with the same syntax in each
556-
case. We will discuss them in the following order: first, parametric composite types, then parametric
557-
abstract types, and finally parametric primitive types.
542+
each possible combination of parameter values. This is often referred to as "generic programming".
543+
There are two kinds of explicitly declared parametric types.
544+
[Parametric composite types](#man-parametric-composite-types)
545+
declare a family of concrete, [composite types](#Composite-Types),
546+
whereas [parametric abstract types](#Parametric-Abstract-Types)
547+
declare a family of [abstract types](#man-abstract-types).
548+
Other kinds of built-in parametric types such as tuple types are discussed in this section as well.
549+
Parametric types can also be used in [type annotations](#Type-Annotations).
550+
551+
!!! note
552+
Parametric types do not represent a single node in the type graph.
553+
Therefore, [`typeof`](@ref) can never return a parametric type name.
554+
To dynamically verify a (parametric) subtype relation, use [`isa`](@ref) instead.
555+
556+
!!! warning
557+
Type parameters are invariant: even though `Float64 <: Real`,
558+
the relation `Foo{Float64} <: Foo{Real}` does NOT hold for any parametric type `Foo`.
559+
However, `Foo{Float64} <: Foo{<:Real}` is `true`.
560+
This subtle but important point is the reason for the common use of
561+
`Container{<:Element}` syntax in type annotations.
558562

559563
### [Parametric Composite Types](@id man-parametric-composite-types)
560564

561-
Type parameters are introduced immediately after the type name, surrounded by curly braces:
565+
!!! note
566+
For any parametric composite type,
567+
both [`isabstracttype`](@ref) and [`isconcretetype`](@ref) will return false.
568+
569+
Type parameters are introduced immediately after the type name, and surrounded by curly braces:
562570

563571
```jldoctest pointtype
564572
julia> struct Point{T}
@@ -567,13 +575,13 @@ julia> struct Point{T}
567575
end
568576
```
569577

570-
This declaration defines a new parametric type, `Point{T}`, holding two "coordinates" of type
571-
`T`. What, one may ask, is `T`? Well, that's precisely the point of parametric types: it can be
572-
any type at all (or a value of any bits type, actually, although here it's clearly used as a type).
573-
`Point{Float64}` is a concrete type equivalent to the type defined by replacing `T` in the definition
574-
of `Point` with [`Float64`](@ref). Thus, this single declaration actually declares an unlimited
575-
number of types: `Point{Float64}`, `Point{AbstractString}`, `Point{Int64}`, etc. Each of these
576-
is now a usable concrete type:
578+
This declaration defines a new parametric type, `Point{T}`,
579+
holding two "coordinates" of the same type `T`.
580+
The parameter `T` is a placeholder, which will be replaced by a particular concrete type
581+
when an instance of this type is created.
582+
Thus, this single declaration actually declares an unlimited number of types:
583+
`Point{Float64}`, `Point{AbstractString}`, `Point{Int64}`, etc.
584+
Each of these is now a usable concrete type:
577585

578586
```jldoctest pointtype
579587
julia> Point{Float64}
@@ -583,11 +591,8 @@ julia> Point{AbstractString}
583591
Point{AbstractString}
584592
```
585593

586-
The type `Point{Float64}` is a point whose coordinates are 64-bit floating-point values, while
587-
the type `Point{AbstractString}` is a "point" whose "coordinates" are string objects (see [Strings](@ref)).
588-
589-
`Point` itself is also a valid type object, containing all instances `Point{Float64}`, `Point{AbstractString}`,
590-
etc. as subtypes:
594+
`Point` itself is also a valid type object,
595+
containing all instances `Point{Float64}`, `Point{AbstractString}`, etc. as subtypes:
591596

592597
```jldoctest pointtype
593598
julia> Point{Float64} <: Point
@@ -597,17 +602,7 @@ julia> Point{AbstractString} <: Point
597602
true
598603
```
599604

600-
Other types, of course, are not subtypes of it:
601-
602-
```jldoctest pointtype
603-
julia> Float64 <: Point
604-
false
605-
606-
julia> AbstractString <: Point
607-
false
608-
```
609-
610-
Concrete `Point` types with different values of `T` are never subtypes of each other:
605+
Concrete `Point` variants with different values of `T` are never subtypes of each other:
611606

612607
```jldoctest pointtype
613608
julia> Point{Float64} <: Point{Int64}
@@ -617,13 +612,9 @@ julia> Point{Float64} <: Point{Real}
617612
false
618613
```
619614

620-
!!! warning
621-
This last point is *very* important: even though `Float64 <: Real` we **DO NOT** have `Point{Float64} <: Point{Real}`.
622-
623-
In other words, in the parlance of type theory, Julia's type parameters are *invariant*, rather
624-
than being [covariant (or even contravariant)](https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29). This is for practical reasons: while any instance
625-
of `Point{Float64}` may conceptually be like an instance of `Point{Real}` as well, the two types
626-
have different representations in memory:
615+
In other words, in the parlance of type theory, Julia's type parameters are *invariant*.
616+
This is for practical reasons: while any instance of `Point{Float64}` may conceptually be
617+
like an instance of `Point{Real}` as well, the two types have different representations in memory:
627618

628619
* An instance of `Point{Float64}` can be represented compactly and efficiently as an immediate pair
629620
of 64-bit values;
@@ -632,9 +623,9 @@ have different representations in memory:
632623
practice an instance of `Point{Real}` must be represented as a pair of pointers to
633624
individually allocated `Real` objects.
634625

635-
The efficiency gained by being able to store `Point{Float64}` objects with immediate values is
636-
magnified enormously in the case of arrays: an `Array{Float64}` can be stored as a contiguous
637-
memory block of 64-bit floating-point values, whereas an `Array{Real}` must be an array of pointers
626+
The efficiency gained by imposing this restriction is magnified enormously in the case of arrays:
627+
an `Array{Float64}` can be stored as a contiguous memory block of 64-bit floating-point values,
628+
whereas an `Array{Real}` must be an array of pointers
638629
to individually allocated [`Real`](@ref) objects -- which may well be
639630
[boxed](https://en.wikipedia.org/wiki/Object_type_%28object-oriented_programming%29#Boxing)
640631
64-bit floating-point values, but also might be arbitrarily large, complex objects, which are
@@ -663,8 +654,7 @@ end
663654

664655
More examples will be discussed later in [Methods](@ref).
665656

666-
How does one construct a `Point` object? It is possible to define custom constructors for composite
667-
types, which will be discussed in detail in [Constructors](@ref man-constructors), but in the absence of any special
657+
How does one construct a `Point` object? In the absence of any special
668658
constructor declarations, there are two default ways of creating new composite objects, one in
669659
which the type parameters are explicitly given and the other in which they are implied by the
670660
arguments to the object constructor.
@@ -724,8 +714,23 @@ Closest candidates are:
724714
Point(::T, !Matched::T) where T at none:2
725715
```
726716

727-
Constructor methods to appropriately handle such mixed cases can be defined, but that will not
728-
be discussed until later on in [Constructors](@ref man-constructors).
717+
For that to work, we could change our definition of the `Point` type:
718+
719+
```jldoctest
720+
julia> struct Point{T1,T2}
721+
x::T1
722+
y::T2
723+
end
724+
725+
julia> p = Point(1, 2.5)
726+
Point{Int64, Float64}(1, 2.5)
727+
728+
julia> p2 = Point(1, 2)
729+
Point{Int64, Int64}(1, 2)
730+
```
731+
732+
Constructor methods to appropriately handle more complex cases can also be defined,
733+
see the [Constructors](@ref man-constructors) section.
729734

730735
### Parametric Abstract Types
731736

0 commit comments

Comments
 (0)