737 lines
19 KiB
Plaintext
737 lines
19 KiB
Plaintext
---
|
||
notebook-links: false
|
||
engine: julia
|
||
---
|
||
|
||
```{julia}
|
||
#| error: false
|
||
#| echo: false
|
||
#| output: false
|
||
using InteractiveUtils
|
||
import QuartoNotebookWorker
|
||
Base.stdout = QuartoNotebookWorker.with_context(stdout)
|
||
myactive_module() = Main.Notebook
|
||
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.
|
||
|
||
However, we will now take a look under the hood.
|
||
|
||
## 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.
|
||
|
||
```{julia}
|
||
subtypes(Int64)
|
||
```
|
||
The result is an empty list of types. `Int64` is a so-called **concrete type** and has 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).
|
||
```{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)
|
||
```
|
||
|
||
Now one can look at the nodes:
|
||
|
||
{{< embed ../notebooks/nb-types.ipynb#nb3 >}}
|
||
|
||
With a small recursive function, one can quickly print out an entire (sub-)tree:
|
||
|
||
{{< 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 >}}
|
||
|
||
::::
|
||
|
||
|
||
Here again as an image (made with LaTeX/[TikZ](https://tikz.dev/tikz-trees))
|
||
|
||
::: {.content-visible when-format="html"}
|
||
{width=80%}
|
||
:::
|
||
|
||
::: {.content-visible when-format="pdf"}
|
||
{width=60%}
|
||
:::
|
||
|
||
|
||
Of course, Julia has not only numeric types. The number of direct descendants (children) of `Any` is
|
||
|
||
```{julia}
|
||
length(subtypes(Any))
|
||
```
|
||
and with (almost) every package loaded with `using ...`, this increases.
|
||
|
||
## 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, i.e., there are no objects of this type.
|
||
- 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:
|
||
|
||
```{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?`.
|
||
|
||
```{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, their definition is quite simple. 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)
|
||
```
|
||
|
||
With the definition, the abstract types are "hung" at a 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:
|
||
|
||
`Bool` is numeric in the sense of `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 `ℯ`.
|
||
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.
|
||
|
||
```{julia}
|
||
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
|
||
```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.
|
||
:::
|
||
|
||
## Composite (_composite_) Types: `struct`
|
||
|
||
A `struct` is a collection of several named fields and defines a concrete type.
|
||
|
||
```{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 we have already seen with expressions of the form `x = Int8(33)`, type names can be used directly as constructors:
|
||
|
||
```{julia}
|
||
p1 = Point2D(1.4, 3.5)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
p1 isa Point3D, p1 isa Point2D, p1 isa Point
|
||
```
|
||
|
||
|
||
The fields of a `struct` can be addressed by their name with 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.
|
||
|
||
```{julia}
|
||
p1.x = 3333.4
|
||
p1
|
||
```
|
||
|
||
Information about the structure of a type or an object of that type is provided by `dump()`.
|
||
|
||
```{julia}
|
||
dump(Point3D)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
dump(p1)
|
||
```
|
||
|
||
## Functions and *Multiple Dispatch*
|
||
|
||
:::{.callout-note .titlenormal}
|
||
|
||
## Objects, Functions, Methods
|
||
|
||
In classical object-oriented languages like C++/Java, objects usually have functions associated with them, which are the methods of the object.
|
||
|
||
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.)
|
||
|
||
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.
|
||
|
||
:::
|
||
|
||
|
||
We implement the distance between two points as a 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 macro `@which`, applied to a full function call with concrete argument list, shows which method is selected for these concrete arguments:
|
||
|
||
```{julia}
|
||
@which sqrt(3.3)
|
||
```
|
||
```{julia}
|
||
z = "Hello" * '!'
|
||
println(z)
|
||
|
||
@which "Hello" * '!'
|
||
```
|
||
|
||
Methods can also have abstract types as arguments:
|
||
|
||
```{julia}
|
||
"""
|
||
Calculates the angle ϕ (in degrees) of the polar coordinates (2D) or
|
||
spherical coordinates (3D) of a point
|
||
"""
|
||
function phi_winkel(p::Point)
|
||
atand(p.y, p.x)
|
||
end
|
||
|
||
phi_winkel(p1)
|
||
```
|
||
|
||
:::{.callout-tip collapse="true"}
|
||
A text enclosed in *triple quotes* immediately before the function definition
|
||
is automatically integrated into Julia's help database:
|
||
|
||
```{julia}
|
||
?phi_winkel
|
||
```
|
||
:::
|
||
|
||
|
||
|
||
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):
|
||
|
||
```{julia}
|
||
f(x::String, y::Number) = "Args: String + Zahl"
|
||
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"
|
||
|
||
function f(x::Number, y::Number, z::String)
|
||
return "Arg: 2 x Zahl + String"
|
||
end
|
||
```
|
||
Here, the first two methods match. The second is chosen since 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.
|
||
|
||
```{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 called `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.
|
||
|
||
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` resp. `Complex`.
|
||
|
||
|
||
```{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`.
|
||
|
||
Now ℚ$\subset$ ℝ, or in Julia notation `Rational <: Real`. Thus, the components of a complex number can also be rational:
|
||
|
||
```{julia}
|
||
z = 3//4 + 5im
|
||
dump(z)
|
||
```
|
||
|
||
|
||
|
||
|
||
These structures are defined without the `mutable` attribute, therefore *immutable*:
|
||
|
||
```{julia}
|
||
x = 2.2 + 3.3im
|
||
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.
|
||
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.
|
||
|
||
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:
|
||
|
||
```{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. They are objects of one of the 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 3 concrete "meta-types" are, by the way, subtypes of the abstract "meta-type" `Type`.
|
||
|
||
```{julia}
|
||
subtypes(Type)
|
||
```
|
||
|
||
-----------------
|
||
|
||
(2) Thus, types can also be easily assigned to variables:
|
||
|
||
```{julia}
|
||
x3 = Float64
|
||
@show x3(4) x3 <: Real 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.
|
||
|
||
One should still follow them to keep the code readable.
|
||
|
||
:::
|
||
|
||
|
||
If such assignments are declared permanent with `const`, a
|
||
*type alias* is created.
|
||
|
||
|
||
```{julia}
|
||
const MyCmplxF64 = MyComplex{Float64}
|
||
|
||
z = MyCmplxF64(1.1, 2.2)
|
||
typeof(z)
|
||
```
|
||
|
||
--------
|
||
|
||
(3) Types can be arguments of functions.
|
||
|
||
|
||
```{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);
|
||
```
|
||
|
||
If one wants to define this function with type signatures, of course one 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, where one 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:
|
||
|
||
```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 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)`.
|
||
|
||
|
||
|
||
|
||
## 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}}`?
|
||
|
||
Yes, they exist; and they are concrete types, one can therefore create objects of this type.
|
||
|
||
```{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.
|
||
|
||
Now it holds 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 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**.)
|
||
|
||
|
||
|
||
|
||
## Generic Functions
|
||
|
||
The usual (and in many cases recommended!) programming style in Julia is writing generic functions:
|
||
|
||
```{julia}
|
||
function fsinnfrei1(x, y)
|
||
return x * x * y
|
||
end
|
||
```
|
||
This function works immediately with all types for which the used operations are defined.
|
||
|
||
```{julia}
|
||
fsinnfrei1( Complex(2,3), 10), fsinnfrei1("Hello", '!')
|
||
```
|
||
|
||
|
||
One can of course use type annotations to restrict usability or to implement different methods for different types:
|
||
|
||
```{julia}
|
||
function fsinnfrei2(x::Number, y::AbstractFloat)
|
||
return x * x * y
|
||
end
|
||
|
||
function fsinnfrei2(x::String, y::String)
|
||
println("Sorry, I don't take strings!")
|
||
end
|
||
|
||
|
||
@show fsinnfrei2(18, 2.0) fsinnfrei2(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.
|
||
|
||
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.
|
||
|
||
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
|
||
|
||
fsinnfrei1(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
|
||
|
||
```{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 and both must be subtypes 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.
|
||
|
||
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:
|
||
|
||
```{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.
|
||
|
||
```{julia}
|
||
fgl(33, 44)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
fgl(33, 44.0)
|
||
```
|
||
|
||
|