english improved
This commit is contained in:
@@ -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"}
|
||||
{width=80%}
|
||||
:::
|
||||
|
||||
::: {.content-visible when-format="pdf"}
|
||||
::: {.content-visible when-format="typst"}
|
||||
{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)
|
||||
|
||||
Reference in New Issue
Block a user