745 lines
19 KiB
Plaintext
745 lines
19 KiB
Plaintext
---
|
||
notebook-links: false
|
||
engine: julia
|
||
---
|
||
|
||
```{julia}
|
||
#| error: false
|
||
#| echo: false
|
||
#| output: false
|
||
using InteractiveUtils
|
||
|
||
#struct M a::Int end; x = M(22); @show x
|
||
#should not print "Main.Notebook.M(22)" but only "M(22)"
|
||
function Base.show(io::IO, x::T) where T
|
||
if parentmodule(T) == @__MODULE__
|
||
# Print "TypeName(fields...)" without module prefix
|
||
print(io, nameof(T), "(")
|
||
fields = fieldnames(T)
|
||
for (i, f) in enumerate(fields)
|
||
print(io, getfield(x, f))
|
||
i < length(fields) && print(io, ", ")
|
||
end
|
||
print(io, ")")
|
||
else
|
||
invoke(Base.show, Tuple{IO, Any}, io, x)
|
||
end
|
||
end
|
||
```
|
||
|
||
|
||
# The Julia Type System
|
||
|
||
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, 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. `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** with no subtypes.
|
||
|
||
Let's now traverse this branch upward to the root (in computer science, trees are typically inverted).
|
||
```{julia}
|
||
supertype(Int64)
|
||
```
|
||
```{julia}
|
||
supertype(Signed)
|
||
```
|
||
```{julia}
|
||
supertype(Integer)
|
||
```
|
||
```{julia}
|
||
supertype(Real)
|
||
```
|
||
```{julia}
|
||
supertype(Number)
|
||
```
|
||
This would have been faster, by the way: The function `supertypes()` (with plural-s) shows all ancestors.
|
||
|
||
```{julia}
|
||
supertypes(Int64)
|
||
```
|
||
|
||
We can now examine the nodes:
|
||
|
||
{{< embed ../notebooks/nb-types.ipynb#nb3 >}}
|
||
|
||
A simple recursive function can display the entire subtree:
|
||
|
||
{{< embed ../notebooks/nb-types.ipynb#nb1 >}}
|
||
|
||
::::{.content-hidden unless-format="xxx"}
|
||
|
||
...and of course, there is also a Julia package for this:
|
||
|
||
{{< embed ../notebooks/nb-types.ipynb#nb2 >}}
|
||
|
||
::::
|
||
|
||
|
||
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="typst"}
|
||
{width=60%}
|
||
:::
|
||
|
||
|
||
Beyond numeric types, Julia includes many others. The number of direct descendants (children) of `Any` is
|
||
|
||
```{julia}
|
||
length(subtypes(Any))
|
||
```
|
||
This number increases with (almost) every package loaded via `using ...`.
|
||
|
||
## Abstract and Concrete Types
|
||
|
||
- An object always has a **concrete** type.
|
||
- Concrete types have no more subtypes, they are always the "leaves" of the tree.
|
||
- Concrete types specify a concrete data structure.
|
||
|
||
:::{.xxx}
|
||
|
||
:::
|
||
|
||
- 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.
|
||
|
||
|
||
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 reads as the question `is x a T?`.
|
||
|
||
```{julia}
|
||
x = 17.2
|
||
|
||
42 isa Int64, 42 isa Real, x isa Real, x isa Float64, x isa Integer
|
||
```
|
||
|
||
|
||
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
|
||
|
||
supertype(MySuperType)
|
||
```
|
||
|
||
or from another abstract type:
|
||
|
||
```{julia}
|
||
abstract type MySpecialNumber <: Integer end
|
||
|
||
supertypes(MySpecialNumber)
|
||
```
|
||
|
||
By this definition, the abstract type is attached at a specific point in the type tree.
|
||
|
||
## The Numeric Types `Bool` and `Irrational`
|
||
|
||
Though appearing in the numeric type tree, these types warrant brief explanation:
|
||
|
||
`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 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
|
||
|
||
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}
|
||
```
|
||
|
||
:::{.callout-note .titlenormal}
|
||
|
||
## Example
|
||
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 comparing fixed-width machine numbers with arbitrary precision numbers.
|
||
:::
|
||
|
||
## Composite Types: `struct`
|
||
|
||
A `struct` defines a concrete type as a collection of named fields.
|
||
|
||
```{julia}
|
||
abstract type Point end
|
||
|
||
mutable struct Point2D <: Point
|
||
x :: Float64
|
||
y :: Float64
|
||
end
|
||
|
||
mutable struct Point3D <: Point
|
||
x :: Float64
|
||
y :: Float64
|
||
z :: Float64
|
||
end
|
||
```
|
||
|
||
As seen with expressions like `x = Int8(33)`, type names can serve as constructors:
|
||
|
||
```{julia}
|
||
p1 = Point2D(1.4, 3.5)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
p1 isa Point3D, p1 isa Point2D, p1 isa Point
|
||
```
|
||
|
||
|
||
The fields of a `struct` are accessed by name using the `.` operator.
|
||
|
||
```{julia}
|
||
p1.y
|
||
```
|
||
|
||
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
|
||
```
|
||
|
||
The `dump()` function displays structure information for types and objects.
|
||
|
||
```{julia}
|
||
dump(Point3D)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
dump(p1)
|
||
```
|
||
|
||
## Functions and *Multiple Dispatch*
|
||
|
||
:::{.callout-note .titlenormal}
|
||
|
||
## Objects, Functions, and Methods
|
||
|
||
In classical object-oriented languages (C++, Java, Python), methods are bound to objects.
|
||
|
||
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.
|
||
|
||
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 define a distance function with two methods:
|
||
|
||
```{julia}
|
||
function distance(p1::Point2D, p2::Point2D)
|
||
sqrt((p1.x-p2.x)^2 + (p1.y-p2.y)^2)
|
||
end
|
||
|
||
function distance(p1::Point3D, p2::Point3D)
|
||
sqrt((p1.x-p2.x)^2 + (p1.y-p2.y)^2 + (p1.z-p2.z)^2)
|
||
end
|
||
```
|
||
```{julia}
|
||
distance(p1, Point2D(2200, -300))
|
||
```
|
||
|
||
As mentioned earlier, `methods()` shows the method table of a function:
|
||
|
||
```{julia}
|
||
methods(distance)
|
||
```
|
||
|
||
The `@which` macro, applied to a function call with concrete arguments, shows which method is selected for these arguments:
|
||
|
||
```{julia}
|
||
@which sqrt(3.3)
|
||
```
|
||
```{julia}
|
||
z = "Hello" * '!'
|
||
println(z)
|
||
|
||
@which "Hello" * '!'
|
||
```
|
||
|
||
Methods can also have abstract types as arguments:
|
||
|
||
```{julia}
|
||
"""
|
||
Calculate the angle ϕ (in degrees) of the polar coordinates (2D) or
|
||
spherical coordinates (3D) of a point
|
||
"""
|
||
function phi_angle(p::Point)
|
||
atand(p.y, p.x)
|
||
end
|
||
|
||
phi_angle(p1)
|
||
```
|
||
|
||
:::{.callout-tip collapse="true"}
|
||
Text enclosed in *triple quotes* immediately before the function definition
|
||
is automatically integrated into Julia's help database:
|
||
|
||
```{julia}
|
||
?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 written in short [*assignment form*](https://docs.julialang.org/en/v1/manual/functions/#man-functions)):
|
||
|
||
```{julia}
|
||
f(x::String, y::Number) = "Args: String + Number"
|
||
f(x::String, y::Int64) = "Args: String + Int64"
|
||
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 × Number + String"
|
||
end
|
||
```
|
||
The first two methods match; the second is chosen as it is more specific (`Int64 <: Number`):
|
||
```{julia}
|
||
f("Hello", 42)
|
||
```
|
||
Ambiguities may arise if methods are defined poorly:
|
||
|
||
```{julia}
|
||
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 denoted `im`
|
||
|
||
```{julia}
|
||
@show Complex(0.4) 23 + 0.5im/(1-2im);
|
||
```
|
||
|
||
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.
|
||
|
||
```{julia}
|
||
x = 2//7
|
||
@show typeof(x);
|
||
```
|
||
```{julia}
|
||
y = BigInt(2)//7
|
||
@show typeof(y) y^48;
|
||
```
|
||
|
||
|
||
```{julia}
|
||
x = 1 + 2im
|
||
typeof(x)
|
||
```
|
||
```{julia}
|
||
y = 1.0 + 2.0im
|
||
typeof(y)
|
||
```
|
||
|
||
The concrete types `Rational{Int64}`, `Rational{BigInt}`,..., `Complex{Int64}`, `Complex{Float64}}`,... are subtypes of `Rational` and `Complex`, respectively.
|
||
|
||
|
||
```{julia}
|
||
Rational{BigInt} <: Rational
|
||
```
|
||
|
||
The definitions [look roughly like this](https://github.com/JuliaLang/julia/blob/master/base/rational.jl#L6-L15):
|
||
|
||
```{julia}
|
||
struct MyComplex{T<:Real} <: Number
|
||
re::T
|
||
im::T
|
||
end
|
||
|
||
struct MyRational{T<:Integer} <: Real
|
||
num::T
|
||
den::T
|
||
end
|
||
```
|
||
|
||
The first definition says:
|
||
|
||
- `MyComplex` has two fields `re` and `im`, both of the same type `T`.
|
||
- This type `T` must be a subtype of `Real`.
|
||
- `MyComplex` and all its variants like `MyComplex{Float64}` are subtypes of `Number`.
|
||
|
||
and the second says analogously:
|
||
|
||
- `MyRational` has two fields `num` and `den`, both of the same type `T`.
|
||
- This type `T` must be a subtype of `Integer`.
|
||
- `MyRational` and its variants are subtypes of `Real`.
|
||
|
||
Since ℚ$\subset$ ℝ (or `Rational <: Real` in Julia notation), the components of a complex number can also be rational:
|
||
|
||
```{julia}
|
||
z = 3//4 + 5im
|
||
dump(z)
|
||
```
|
||
|
||
|
||
|
||
|
||
These structures are declared without the `mutable` attribute, making them *immutable*:
|
||
|
||
```{julia}
|
||
x = 2.2 + 3.3im
|
||
println("The real part is: $(x.re)")
|
||
|
||
x.re = 4.4
|
||
```
|
||
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` then references this new object.
|
||
|
||
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}}}
|
||
param::T
|
||
end
|
||
|
||
p1 = MyParms(33.3)
|
||
p2 = MyParms( (2, 4) )
|
||
|
||
@show p1.param p2.param;
|
||
```
|
||
|
||
|
||
|
||
|
||
## Types as Objects
|
||
|
||
(1) Types are also objects. Each type is an instance of one of three "meta-types":
|
||
|
||
- `Union` (union types)
|
||
- `UnionAll` (parametric types)
|
||
- `DataType` (all concrete and other abstract types)
|
||
|
||
```{julia}
|
||
@show 23779 isa Int64 Int64 isa DataType;
|
||
```
|
||
|
||
```{julia}
|
||
@show 2im isa Complex Complex isa UnionAll;
|
||
```
|
||
|
||
|
||
```{julia}
|
||
@show 2im isa Complex{Int64} Complex{Int64} isa DataType;
|
||
```
|
||
|
||
|
||
These three concrete "meta-types" are, by the way, subtypes of the abstract "meta-type" `Type`.
|
||
|
||
```{julia}
|
||
subtypes(Type)
|
||
```
|
||
|
||
-----------------
|
||
|
||
(2) Types can be assigned to a variable:
|
||
|
||
```{julia}
|
||
x3 = Float64
|
||
@show x3(4) x3 <: Real x3==Float64 ;
|
||
```
|
||
|
||
:::{.callout-note collapse="true"}
|
||
|
||
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.
|
||
|
||
They should still be followed for readability.
|
||
|
||
:::
|
||
|
||
Declaring such assignments with `const` creates a *type alias*.
|
||
|
||
|
||
```{julia}
|
||
const MyCmplxF64 = MyComplex{Float64}
|
||
|
||
z = MyCmplxF64(1.1, 2.2)
|
||
typeof(z)
|
||
```
|
||
|
||
--------
|
||
|
||
(3) Types can be function arguments.
|
||
|
||
|
||
```{julia}
|
||
function myf(x, S, T)
|
||
if S <: T
|
||
println("$S is subtype of $T")
|
||
end
|
||
return S(x)
|
||
end
|
||
|
||
z = myf(43, UInt16, Real)
|
||
|
||
@show z typeof(z);
|
||
```
|
||
|
||
To define this function with type signatures, we can write
|
||
```julia
|
||
function myf(x, S::Type, T::Type) ... end
|
||
```
|
||
However, the (equivalent) special syntax
|
||
```julia
|
||
function myf(x, ::Type{S}, ::Type{T}) where {S,T} ... end
|
||
```
|
||
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 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
|
||
```
|
||
`Type{Int64}` acts like a "meta-type", whose only instance is the type `Int64`.
|
||
|
||
|
||
-----------------
|
||
|
||
(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 used as parameters in parametric types? Are there `Complex{AbstractFloat}` or `Complex{Union{Float32, Int16}}`?
|
||
|
||
Yes, such types exist. They are concrete, and objects can be instantiated.
|
||
|
||
```{julia}
|
||
z5 = Complex{Integer}(2, 0x33)
|
||
dump(z5)
|
||
```
|
||
This is a heterogeneous composite type. Each component has an individual type `T`, for which `T<:Integer` holds.
|
||
|
||
Note that
|
||
|
||
```{julia}
|
||
Int64 <: Integer
|
||
```
|
||
but it does not hold that
|
||
```{julia}
|
||
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 theoretical computer science terminology](https://en.wikipedia.org/wiki/Type_variance), **invariant**.
|
||
(If `S<:T` implied `ParamType{S} <: ParamType{T}`, this would be **covariance**.)
|
||
|
||
|
||
|
||
## Generic Functions
|
||
|
||
The usual (and in many cases recommended!) programming style in Julia is writing generic functions:
|
||
|
||
```{julia}
|
||
function fmm(x, y)
|
||
return x * x * y
|
||
end
|
||
```
|
||
This function works with any types supporting the required operations.
|
||
|
||
```{julia}
|
||
fmm( Complex(2,3), 10), fmm("Hello", '!')
|
||
```
|
||
|
||
|
||
Type annotations can restrict applicability or implement different methods for different types:
|
||
|
||
```{julia}
|
||
function fmm2(x::Number, y::AbstractFloat)
|
||
return x * x * y
|
||
end
|
||
|
||
function fmm2(x::String, y::String)
|
||
println("Sorry, I don't take strings!")
|
||
end
|
||
|
||
|
||
@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 *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 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:
|
||
|
||
```{julia}
|
||
#| echo: false
|
||
#| output: false
|
||
|
||
#=
|
||
-Measurements.jl/show.jl adds a method
|
||
Base.show(io::IO, ::MIME"text/latex", measure::Measurement)
|
||
- IJulia sends out a text/latex-cell in addition to the text/plain-cell
|
||
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/latex": [
|
||
"$2590.0 \\pm 52.0$"
|
||
],
|
||
"text/plain": [
|
||
"2590.0 ± 52.0"
|
||
]
|
||
},
|
||
whenever it finds this method, see
|
||
https://github.com/JuliaLang/IJulia.jl/commit/7311e517194ddba07af64952b0f9413401213050
|
||
|
||
- Quarto takes this latex-cell in its qmd -> ipynb->exec->ipynb -> md pipeline, generating a :::{=tex} -div
|
||
|
||
- so we delete the method
|
||
|
||
=#
|
||
using Measurements
|
||
zz = @which Base.show(stdout, MIME"text/latex"(), 3±2)
|
||
Base.delete_method(zz)
|
||
```
|
||
|
||
```{julia}
|
||
using Measurements
|
||
|
||
x = 33.56±0.3
|
||
y = 2.3±0.02
|
||
|
||
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., an implementation of [prime factorization in ℤ[i]](https://en.wikipedia.org/wiki/Gaussian_integer). The definition
|
||
|
||
```{julia}
|
||
#| eval: false
|
||
function isprime(x::Complex{Integer}) ... end
|
||
```
|
||
|
||
does not give the desired result, as we saw in @sec-invariance. The function would not work for an argument of type `Complex{Int64}`, since the latter is not a subtype of `Complex{Integer}`.
|
||
|
||
We must introduce a type variable. The `where` clause serves this purpose.
|
||
|
||
```{julia}
|
||
#| eval: false
|
||
function isprime(x::Complex{T}) where {T<:Integer}
|
||
...
|
||
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`."
|
||
|
||
Another example:
|
||
```{julia}
|
||
#| eval: false
|
||
function kgV(x::Complex{T}, y::Complex{S}) where {T<:Integer, S<:Integer}
|
||
...
|
||
end
|
||
```
|
||
|
||
> 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
|
||
write
|
||
|
||
```{julia}
|
||
#| eval: false
|
||
function isprime(x::Complex{T}) where T<:Integer
|
||
...
|
||
end
|
||
```
|
||
This can still be shortened to
|
||
```{julia}
|
||
#| eval: false
|
||
function isprime(x::Complex{<:Integer})
|
||
...
|
||
end
|
||
```
|
||
|
||
These different variants can be confusing, but it is only syntax.
|
||
|
||
```{julia}
|
||
C1 = Complex{T} where {T<:Integer}
|
||
C2 = Complex{T} where T<:Integer
|
||
C3 = Complex{<:Integer}
|
||
|
||
C1 == C2 == C3
|
||
```
|
||
|
||
Short syntax for simple cases; extended syntax for complex variants.
|
||
|
||
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 both arguments have exactly the same type; otherwise, any type is accepted.
|
||
|
||
```{julia}
|
||
fgl(33, 44)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
fgl(33, 44.0)
|
||
```
|
||
|
||
|