english improved

This commit is contained in:
2026-03-05 20:09:16 +01:00
parent c6609d15f5
commit 733fe8c290
21 changed files with 954 additions and 1042 deletions

View File

@@ -17,20 +17,20 @@ Base.active_module() = myactive_module()
# The Julia Type System
One can write extensive programs in Julia without using a single type declaration. This is, of course, intentional and should simplify the work of users.
One can write extensive programs in Julia without using a single type declaration. This is, of course, intentional and designed to simplify users' work.
However, we will now take a look under the hood.
However, for a deeper understanding we will now examine the underlying type system.
## The Type Hierarchy: A Case Study with Numeric Types
The type system has the structure of a tree whose root is the type `Any`. The functions `subtypes()` and `supertype()` can be used to explore the tree. They show all children or the parent of a node.
The type system has the structure of a tree whose root is the type `Any`. The functions `subtypes()` and `supertype()` can be used to explore the tree. `subtypes()` displays all child nodes, while `supertype()` shows the parent.
```{julia}
subtypes(Int64)
```
The result is an empty list of types. `Int64` is a so-called **concrete type** and has no subtypes.
The result is an empty list of types. `Int64` is a so-called **concrete type** with no subtypes.
Let's now climb the type hierarchy on this branch to the root (in computer science, trees are always standing on their heads).
Let's now traverse this branch upward to the root (in computer science, trees are typically inverted).
```{julia}
supertype(Int64)
```
@@ -52,11 +52,11 @@ This would have been faster, by the way: The function `supertypes()` (with plura
supertypes(Int64)
```
Now one can look at the nodes:
We can now examine the nodes:
{{< embed ../notebooks/nb-types.ipynb#nb3 >}}
With a small recursive function, one can quickly print out an entire (sub-)tree:
A simple recursive function can display the entire subtree:
{{< embed ../notebooks/nb-types.ipynb#nb1 >}}
@@ -69,23 +69,23 @@ With a small recursive function, one can quickly print out an entire (sub-)tree:
::::
Here again as an image (made with LaTeX/[TikZ](https://tikz.dev/tikz-trees))
Below is the same hierarchy as an image (made with LaTeX/[TikZ](https://tikz.dev/tikz-trees)):
::: {.content-visible when-format="html"}
![](../images/TypeTree2.png){width=80%}
:::
::: {.content-visible when-format="pdf"}
::: {.content-visible when-format="typst"}
![The hierarchy of numeric types](../images/TypeTree2.png){width=60%}
:::
Of course, Julia has not only numeric types. The number of direct descendants (children) of `Any` is
Beyond numeric types, Julia includes many others. The number of direct descendants (children) of `Any` is
```{julia}
length(subtypes(Any))
```
and with (almost) every package loaded with `using ...`, this increases.
This number increases with (almost) every package loaded via `using ...`.
## Abstract and Concrete Types
@@ -97,18 +97,18 @@ and with (almost) every package loaded with `using ...`, this increases.
:::
- Abstract types cannot be instantiated, i.e., there are no objects of this type.
- Abstract types cannot be instantiated; that is, no objects can have an abstract type directly.
- They define a set of concrete types and common methods for these types.
- They can therefore be used in the definition of function types, argument types, element types of composite types, etc.
For **declaring** *and* **testing** the "descent" within the type hierarchy, there is a special operator:
To **declare** *and* **test** the relationships in the type hierarchy, Julia provides a special operator:
```{julia}
Int64 <: Number
```
To test whether an object has a certain type (or an abstract supertype of it), `isa(object, typ)` is used. It is usually used in infix form and should be read as the question `x is a T?`.
To test whether an object has a certain type (or an abstract supertype of it), `isa(object, typ)` is used. It is usually used in infix form and reads as the question `is x a T?`.
```{julia}
x = 17.2
@@ -117,7 +117,7 @@ x = 17.2
```
Since abstract types do not define data structures, their definition is quite simple. Either they are derived directly from `Any`:
Since abstract types do not define data structures, they are simple to define. Either they are derived directly from `Any`:
```{julia}
abstract type MySuperType end
@@ -133,24 +133,24 @@ abstract type MySpecialNumber <: Integer end
supertypes(MySpecialNumber)
```
With the definition, the abstract types are "hung" at a point in the type tree.
By this definition, the abstract type is attached at a specific point in the type tree.
## The Numeric Types `Bool` and `Irrational`
Since they are seen in the numeric type tree, they should be briefly explained:
Though appearing in the numeric type tree, these types warrant brief explanation:
`Bool` is numeric in the sense of `true=1, false=0`:
`Bool` is numeric in the sense that `true=1, false=0`:
```{julia}
true + true + true, false - true, sqrt(true), true/4
```
`Irrational` is the type of some predefined constants such as `π` and ``.
`Irrational` is the type of certain predefined constants, such as `π` and ``.
According to the [documentation](https://docs.julialang.org/en/v1/base/numbers/#Base.AbstractIrrational), `Irrational` is a *"Number type representing an exact irrational value, which is automatically rounded to the correct precision in arithmetic operations with other numeric quantities".*
## Union Types
If the tree hierarchy is not sufficient, one can also define abstract types as a union of arbitrary (abstract and concrete) types.
When the tree structure is insufficient, abstract types can be defined as a union of arbitrary (abstract and concrete) types.
```{julia}
IntOrString = Union{Int64,String}
@@ -159,16 +159,16 @@ IntOrString = Union{Int64,String}
:::{.callout-note .titlenormal}
## Example
The command `methods(<)` shows that among the over 70 methods defined for the comparison operator, some also use *union types*, e.g., there is
The command `methods(<)` reveals over 70 methods for the comparison operator, including methods with union type arguments, such as:
```julia
<(x::Union{Float16, Float32, Float64}, y::BigFloat)
```
a method for comparing a machine number of fixed length with a machine number of arbitrary length.
a method comparing fixed-width machine numbers with arbitrary precision numbers.
:::
## Composite (_composite_) Types: `struct`
## Composite Types: `struct`
A `struct` is a collection of several named fields and defines a concrete type.
A `struct` defines a concrete type as a collection of named fields.
```{julia}
abstract type Point end
@@ -185,7 +185,7 @@ mutable struct Point3D <: Point
end
```
As we have already seen with expressions of the form `x = Int8(33)`, type names can be used directly as constructors:
As seen with expressions like `x = Int8(33)`, type names can serve as constructors:
```{julia}
p1 = Point2D(1.4, 3.5)
@@ -197,20 +197,20 @@ p1 isa Point3D, p1 isa Point2D, p1 isa Point
```
The fields of a `struct` can be addressed by their name with the `.` operator.
The fields of a `struct` are accessed by name using the `.` operator.
```{julia}
p1.y
```
Since we declared our `struct` as `mutable`, we can modify the object `p1` by assigning new values to the fields.
Because we declared our `struct` as `mutable`, we can modify the object `p1` by assigning new values to the fields.
```{julia}
p1.x = 3333.4
p1
```
Information about the structure of a type or an object of that type is provided by `dump()`.
The `dump()` function displays structure information for types and objects.
```{julia}
dump(Point3D)
@@ -225,24 +225,23 @@ dump(Point3D)
:::{.callout-note .titlenormal}
## Objects, Functions, Methods
## Objects, Functions, and Methods
In classical object-oriented languages like C++/Java, objects usually have functions associated with them, which are the methods of the object.
In classical object-oriented languages (C++, Java, Python), methods are bound to objects.
In Julia, methods belong to a function and not to an object.
(An exception is the constructors, i.e., functions that have the same name as a type and create an object of that type.)
Julia takes a different approach: methods belong to functions, not to objects.
Constructors (functions sharing a type's name that create instances of that type) are the only exception.
Once one has defined a new type, one can add new or existing functions around new methods for this type.
- A function can be defined multiple times for different argument lists (type and number).
- The function then has multiple methods.
- When calling, it is decided based on the concrete arguments which method is used *(multiple dispatch)*.
- It is typical for Julia that for standard functions, many methods are defined. These can be easily extended by further methods for own types.
When we define a new type, we can define functions specific to that type but we can also add additional methods to existing functions.
- A single functions can have multiple methods for different argument types.
- At call time, Julia selects the most specific method matching the concrete argument types *(multiple dispatch)*.
- Core functions in Julia often have numerous predefined methods and third-party packages or user code can extend them adding additional methods.
:::
We implement the distance between two points as a function with two methods:
We define a distance function with two methods:
```{julia}
function distance(p1::Point2D, p2::Point2D)
@@ -263,7 +262,7 @@ As mentioned earlier, `methods()` shows the method table of a function:
methods(distance)
```
The macro `@which`, applied to a full function call with concrete argument list, shows which method is selected for these concrete arguments:
The `@which` macro, applied to a function call with concrete arguments, shows which method is selected for these arguments:
```{julia}
@which sqrt(3.3)
@@ -279,46 +278,46 @@ Methods can also have abstract types as arguments:
```{julia}
"""
Calculates the angle ϕ (in degrees) of the polar coordinates (2D) or
Calculate the angle ϕ (in degrees) of the polar coordinates (2D) or
spherical coordinates (3D) of a point
"""
function phi_winkel(p::Point)
function phi_angle(p::Point)
atand(p.y, p.x)
end
phi_winkel(p1)
phi_angle(p1)
```
:::{.callout-tip collapse="true"}
A text enclosed in *triple quotes* immediately before the function definition
Text enclosed in *triple quotes* immediately before the function definition
is automatically integrated into Julia's help database:
```{julia}
?phi_winkel
?phi_angle
```
:::
With *multiple dispatch*, the method is applied that is the most specific among all matching ones. Here is a function with several methods
(all but the last in the short [*assignment form*](https://docs.julialang.org/en/v1/manual/functions/#man-functions) written):
(all but the last written in short [*assignment form*](https://docs.julialang.org/en/v1/manual/functions/#man-functions)):
```{julia}
f(x::String, y::Number) = "Args: String + Zahl"
f(x::String, y::Number) = "Args: String + Number"
f(x::String, y::Int64) = "Args: String + Int64"
f(x::Number, y::Int64) = "Args: Zahl + Int64"
f(x::Int64, y:: Number) = "Args: Int64 + Zahl"
f(x::Number) = "Arg: eine Zahl"
f(x::Number, y::Int64) = "Args: Number + Int64"
f(x::Int64, y:: Number) = "Args: Int64 + Number"
f(x::Number) = "Arg: a Number"
function f(x::Number, y::Number, z::String)
return "Arg: 2 x Zahl + String"
return "Arg: 2 × Number + String"
end
```
Here, the first two methods match. The second is chosen since it is more specific, `Int64 <: Number`.
The first two methods match; the second is chosen as it is more specific (`Int64 <: Number`):
```{julia}
f("Hello", 42)
```
It may happen that this rule does not lead to a unique result if one chooses one's methods badly.
Ambiguities may arise if methods are defined poorly:
```{julia}
f(42, 42)
@@ -327,20 +326,19 @@ f(42, 42)
## Parametric Numeric Types: `Rational` and `Complex`
- For rational numbers (fractions), Julia uses `//` as an infix constructor:
```{julia}
@show Rational(23, 17) 4//16 + 1//3;
```
- The imaginary unit $\sqrt{-1}$ is called `im`
- The imaginary unit $\sqrt{-1}$ is denoted `im`
```{julia}
@show Complex(0.4) 23 + 0.5im/(1-2im);
```
`Rational` and `Complex` consist, similar to our `Point2D`, of 2 fields: numerator and denominator or real and imaginary part.
Like `Point2D`, both `Rational` and `Complex` consist of two fields: numerator and denominator or real and imaginary parts.
However, the type of these fields is not completely fixed. `Rational` and `Complex` are _parametric_ types.
@@ -363,7 +361,7 @@ y = 1.0 + 2.0im
typeof(y)
```
The concrete types `Rational{Int64}`, `Rational{BigInt}`,..., `Complex{Int64}`, `Complex{Float64}}`, ... are subtypes of `Rational` resp. `Complex`.
The concrete types `Rational{Int64}`, `Rational{BigInt}`,..., `Complex{Int64}`, `Complex{Float64}}`,... are subtypes of `Rational` and `Complex`, respectively.
```{julia}
@@ -396,7 +394,7 @@ and the second says analogously:
- This type `T` must be a subtype of `Integer`.
- `MyRational` and its variants are subtypes of `Real`.
Now $\subset$ , or in Julia notation `Rational <: Real`. Thus, the components of a complex number can also be rational:
Since $\subset$ (or `Rational <: Real` in Julia notation), the components of a complex number can also be rational:
```{julia}
z = 3//4 + 5im
@@ -406,7 +404,7 @@ dump(z)
These structures are defined without the `mutable` attribute, therefore *immutable*:
These structures are declared without the `mutable` attribute, making them *immutable*:
```{julia}
x = 2.2 + 3.3im
@@ -414,15 +412,15 @@ println("The real part is: $(x.re)")
x.re = 4.4
```
This is common. We also consider the object `9` of type `Int64` as immutable.
This is standard practice. The object `9` (of type `Int64`) is also immutable.
The following, of course, still works:
```{julia}
x += 2.2
```
Here, a new object of type `Complex{Float64}` is created and `x` is made a reference to this new object.
Here, a new object of type `Complex{Float64}` is created and `x` then references this new object.
The possibilities of the type system easily invite to play. Here we define a `struct` that can contain either a machine number or a pair of integers:
The type system's flexibility invites experimentation. Here we define a `struct` that can contain either a machine number or a pair of integers:
```{julia}
struct MyParms{T <: Union{Float64, Tuple{Int64, Int64}}}
@@ -440,7 +438,7 @@ p2 = MyParms( (2, 4) )
## Types as Objects
(1) Types are also objects. They are objects of one of the three "meta-types"
(1) Types are also objects. Each type is an instance of one of three "meta-types":
- `Union` (union types)
- `UnionAll` (parametric types)
@@ -460,7 +458,7 @@ p2 = MyParms( (2, 4) )
```
These 3 concrete "meta-types" are, by the way, subtypes of the abstract "meta-type" `Type`.
These three concrete "meta-types" are, by the way, subtypes of the abstract "meta-type" `Type`.
```{julia}
subtypes(Type)
@@ -468,7 +466,7 @@ subtypes(Type)
-----------------
(2) Thus, types can also be easily assigned to variables:
(2) Types can be assigned to a variable:
```{julia}
x3 = Float64
@@ -477,15 +475,13 @@ x3 = Float64
:::{.callout-note collapse="true"}
This also shows that the [style guidelines in Julia](https://docs.julialang.org/en/v1/manual/style-guide/#Use-naming-conventions-consistent-with-Julia-base/) such as "Types and type variables start with uppercase letters, other variables and functions are written in lowercase." are only conventions and are not enforced by the language.
This demonstrates that [Julia's style guidelines](https://docs.julialang.org/en/v1/manual/style-guide/#Use-naming-conventions-consistent-with-Julia-base/) such as "Types and type variables start with uppercase letters, other variables and functions are written in lowercase." are conventions, not language-enforced rules.
One should still follow them to keep the code readable.
They should still be followed for readability.
:::
If such assignments are declared permanent with `const`, a
*type alias* is created.
Declaring such assignments with `const` creates a *type alias*.
```{julia}
@@ -497,7 +493,7 @@ typeof(z)
--------
(3) Types can be arguments of functions.
(3) Types can be function arguments.
```{julia}
@@ -513,7 +509,7 @@ z = myf(43, UInt16, Real)
@show z typeof(z);
```
If one wants to define this function with type signatures, of course one can write
To define this function with type signatures, we can write
```julia
function myf(x, S::Type, T::Type) ... end
```
@@ -521,9 +517,9 @@ However, the (equivalent) special syntax
```julia
function myf(x, ::Type{S}, ::Type{T}) where {S,T} ... end
```
is more common here, where one can also impose restrictions on the permissible values of the type variables `S` and `T` in the `where` clause.
is more common. Here we can also impose restrictions on the permissible values of the type variables `S` and `T` in the `where` clause.
How do I define a special method of `myf` that should only be called when `S` and `T` are equal to `Int64`? This is possible as follows:
How can we define a special method of `myf` that should only be called when `S` and `T` are equal to `Int64`? This is possible as follows:
```julia
function myf(x, ::Type{Int64}, ::Type{Int64}) ... end
@@ -533,24 +529,24 @@ function myf(x, ::Type{Int64}, ::Type{Int64}) ... end
-----------------
(4) There are numerous operations with types as arguments. We have already seen `<:(T1, T2)`, `supertype(T)`, `supertypes(T)`, `subtypes(T)`. Mentioned should be still `typejoin(T1,T2)` (next common ancestor in the type tree) and tests like `isconcretetype(T)`, `isabstracttype(T)`, `isstructtype(T)`.
(4) There are numerous functions with types as arguments. We have already seen `<:(T1, T2)`, `supertype(T)`, `supertypes(T)`, `subtypes(T)`. Other useful operations include `typejoin(T1,T2)` (next common ancestor in the type tree) and tests like `isconcretetype(T)`, `isabstracttype(T)`, `isstructtype(T)`.
## Invariance of Parametric Types {#sec-invariance}
Can non-concrete types also be substituted into parametric types? Are there `Complex{AbstractFloat}` or `Complex{Union{Float32, Int16}}`?
Can non-concrete types also be used as parameters in parametric types? Are there `Complex{AbstractFloat}` or `Complex{Union{Float32, Int16}}`?
Yes, they exist; and they are concrete types, one can therefore create objects of this type.
Yes, such types exist. They are concrete, and objects can be instantiated.
```{julia}
z5 = Complex{Integer}(2, 0x33)
dump(z5)
```
This is a heterogeneous structure. Each component has an individual type `T`, for which `T<:Integer` holds.
This is a heterogeneous composite type. Each component has an individual type `T`, for which `T<:Integer` holds.
Now it holds that
Note that
```{julia}
Int64 <: Integer
@@ -560,9 +556,8 @@ but it does not hold that
Complex{Int64} <: Complex{Integer}
```
These types are both concrete. Therefore, they cannot stand in a sub/supertype relation to each other in Julia's type hierarchy. Julia's parametric types are [in the language of theoretical computer science](https://en.wikipedia.org/wiki/Covariance_and_contravariance) **invariant**.
(If from `S<:T` it would follow that also `ParamType{S} <: ParamType{T}`, one would speak of **covariance**.)
These types are both concrete. Therefore, they cannot stand in a sub/supertype relation to each other in Julia's type hierarchy. Julia's parametric types are [in theoretical computer science terminology](https://en.wikipedia.org/wiki/Type_variance), **invariant**.
(If `S<:T` implied `ParamType{S} <: ParamType{T}`, this would be **covariance**.)
@@ -571,41 +566,41 @@ These types are both concrete. Therefore, they cannot stand in a sub/supertype r
The usual (and in many cases recommended!) programming style in Julia is writing generic functions:
```{julia}
function fsinnfrei1(x, y)
function fmm(x, y)
return x * x * y
end
```
This function works immediately with all types for which the used operations are defined.
This function works with any types supporting the required operations.
```{julia}
fsinnfrei1( Complex(2,3), 10), fsinnfrei1("Hello", '!')
fmm( Complex(2,3), 10), fmm("Hello", '!')
```
One can of course use type annotations to restrict usability or to implement different methods for different types:
Type annotations can restrict applicability or implement different methods for different types:
```{julia}
function fsinnfrei2(x::Number, y::AbstractFloat)
function fmm2(x::Number, y::AbstractFloat)
return x * x * y
end
function fsinnfrei2(x::String, y::String)
function fmm2(x::String, y::String)
println("Sorry, I don't take strings!")
end
@show fsinnfrei2(18, 2.0) fsinnfrei2(18, 2);
@show fmm2(18, 2.0) fmm2(18, 2);
```
:::{.callout-important}
**Explicit type annotations are almost always irrelevant for the speed of the code!**
This is one of the most important *selling points* of Julia.
This is one of the most important *advantages* of Julia.
Once a function is called for the first time with certain types, a specialized form of the function is generated and compiled for these argument types. Thus, generic functions are usually just as fast as the specialized functions one writes in other languages.
Generic functions enable the cooperation of the most diverse packages and a high level of abstraction.
Generic functions enable seamless integration across packages and support high-level abstraction.
A simple example: The `Measurements.jl` package defines a new data type `Measurement`, a value with error, and the arithmetic of this type. Thus, generic functions work automatically:
@@ -637,7 +632,7 @@ A simple example: The `Measurements.jl` package defines a new data type `Measure
=#
using Measurements
zz= @which Base.show(stdout, MIME"text/latex"(), 3±2)
zz = @which Base.show(stdout, MIME"text/latex"(), 3±2)
Base.delete_method(zz)
```
@@ -647,13 +642,13 @@ using Measurements
x = 33.56±0.3
y = 2.3±0.02
fsinnfrei1(x, y)
fmm(x, y)
```
:::
## Type Parameters in Function Definitions: the `where` Clause
We want to write a function that works for **all complex integers** (and only these), e.g., a [possible prime factorization in [i](https://en.wikipedia.org/wiki/Gaussian_integer). The definition
We want to write a function that works for **all complex integers** (and only these), e.g., an implementation of [prime factorization in [i]](https://en.wikipedia.org/wiki/Gaussian_integer). The definition
```{julia}
#| eval: false
@@ -673,7 +668,7 @@ end
This is to be read as:
> "The argument x should be one of the types `Complex{T}`, where the type variable `T` can be any subtype of `Integer`."
> "The argument `x` should be one of the types `Complex{T}`, where the type variable `T` can be any subtype of `Integer`."
Another example:
```{julia}
@@ -683,7 +678,7 @@ function kgV(x::Complex{T}, y::Complex{S}) where {T<:Integer, S<:Integer}
end
```
> The arguments x and y can have different types and both must be subtypes of `Integer`.
> The arguments x and y can have different types, each a subtype of `Integer`.
If there is only one `where` clause as in the last example, one can omit the curly braces and
@@ -713,16 +708,16 @@ C3 = Complex{<:Integer}
C1 == C2 == C3
```
Short syntax for simple cases, extended syntax for complex variants.
Short syntax for simple cases; extended syntax for complex variants.
Last, it should be noted that `where T` is the short form of `where T<:Any`, which introduces a completely unrestricted type variable. Thus, something like this is possible:
Finally, note that `where T` is shorthand for `where T<:Any`, which introduces a completely unrestricted type variable. Thus, something like this is possible:
```{julia}
function fgl(x::T, y::T) where T
println("Congratulations! x and y are of the same type!")
end
```
This method requires that the arguments are exactly the same type, but otherwise arbitrary.
This method requires that both arguments have exactly the same type; otherwise, any type is accepted.
```{julia}
fgl(33, 44)