Type et méthodes#
Julia possède un système de type et de méthode qui lui confère une approche objet.
La fonction typeof()
renvoie le type d’une variable de base Int32
, Float64
… Julia est conçu pour permettre facilement d’étendre l’environnement à de nouveau type de variable.
Le types sont organisés suivant un hiérarchie comme on peut le voir sur l’arborescence partielle ci-dessous
using AbstractTrees
AbstractTrees.children(x::Type) = subtypes(x)
print_tree(Number)
Number
├─ MultiplicativeInverse
│ ├─ SignedMultiplicativeInverse
│ └─ UnsignedMultiplicativeInverse
├─ Complex
└─ Real
├─ AbstractFloat
│ ├─ BigFloat
│ ├─ Float16
│ ├─ Float32
│ └─ Float64
├─ AbstractIrrational
│ └─ Irrational
├─ Integer
│ ├─ Bool
│ ├─ Signed
│ │ ├─ BigInt
│ │ ├─ Int128
│ │ ├─ Int16
│ │ ├─ Int32
│ │ ├─ Int64
│ │ └─ Int8
│ └─ Unsigned
│ ├─ UInt128
│ ├─ UInt16
│ ├─ UInt32
│ ├─ UInt64
│ └─ UInt8
└─ Rational
Dans cette arborescence, certains types sont “abstraits” et d’autres “concrets”.
isconcretetype(Real), isconcretetype(Float64)
(false, true)
Un réel sera forcemment de type concret Float64
ou Float32
par exemple, et pourra être utilisé comme argument par toutes les fonctions acceptant le type abstrait AbstractFloat
Float64 <: AbstractFloat
true
Méthodes#
A chaque fonction est associée une méthode dépendante du type d’entrée comme dans ce qui suit suivant que l’entrée soit un entier ou pas.
function f(x::Any)
sin(x+1)
end
f (generic function with 1 method)
function f(n::Integer)
n
end
f (generic function with 2 methods)
f(3.0)
-0.7568024953079282
methods(f)
- f(n::Integer) in Main at In[6]:1
- f(x) in Main at In[5]:1
f(3)
3
f(im)
1.2984575814159773 + 0.6349639147847361im
f(-2)
-2
+
+ (generic function with 189 methods)
+(1,2)
3
f(sqrt(2))
0.6649143126867011
Construction d’un nouveau Type de variable#
En premier lieu il faut définir un type abstrait puis une instance sous-hiérarchiquement concrète :
abstract type AbstractGrid end # juste en dessous de Any
mutable struct Grid1 <: AbstractGrid
debut::Float64
fin::Float64
n::Int32
end
@show a = Grid1(0, 1, 2)
a = Grid1(0, 1, 2) = Grid1(0.0, 1.0, 2)
Grid1(0.0, 1.0, 2)
a.debut
0.0
a.fin
1.0
a.n
2
Surcharge des opérateurs#
La surcharge des opérations usuelles se fait en définissant une nouvelle méthode associé au nouveau type pour chaque opérateur, commençons par surcharger l’affichage à l’écran de notre nouveau type. Pour cela on va ajouter une méthode à la fonction “show”
function Base.show(io::IO,g::Grid1)
print(io, "Grid 1d : début $(g.debut) , fin $(g.fin) , $(g.n) éléments\n")
end
Base.show(a)
Grid 1d : début 0.0 , fin 1.0 , 2 éléments
println(a)
Grid 1d : début 0.0 , fin 1.0 , 2 éléments
a
Grid 1d : début 0.0 , fin 1.0 , 2 éléments
@show a
a = Grid 1d : début 0.0 , fin 1.0 , 2 éléments
Grid 1d : début 0.0 , fin 1.0 , 2 éléments
Addition, soustraction …#
Ces fonctions sont de la forme +(), -() c’est à dire
import Base:+
function +(g::Grid1, n::Int)
g.n += n
return g
end
+ (generic function with 191 methods)
a = Grid1(0,1,2)
Grid 1d : début 0.0 , fin 1.0 , 2 éléments
a+2
Grid 1d : début 0.0 , fin 1.0 , 4 éléments
a += 1
Grid 1d : début 0.0 , fin 1.0 , 5 éléments
Attention l’addition n’est pas forcément commutative !
try
2 + a
catch e
showerror(stdout, e)
end
MethodError: no method matching +(::Int64, ::Grid1)
Closest candidates are:
+(::Any, ::Any, ::Any, ::Any...)
@ Base operators.jl:587
+(::Real, ::Complex{Bool})
@ Base complex.jl:319
+(::Number, ::LinearAlgebra.UniformScaling)
@ LinearAlgebra ~/.julia/juliaup/julia-1.10.0+0.x64.linux.gnu/share/julia/stdlib/v1.10/LinearAlgebra/src/uniformscaling.jl:145
...
ni unaire !
try
-a
catch e
showerror(stdout, e)
end
MethodError: no method matching -(::Grid1)
Closest candidates are:
-(::Base.CoreLogging.LogLevel, ::Integer)
@ Base logging.jl:132
-(::Bool, ::Complex{Bool})
@ Base complex.jl:307
-(::Bool, ::Bool)
@ Base bool.jl:167
...
Notez le message d’erreur qui est très claire !
try
a+[1,2]
catch e
showerror(stdout, e)
end
MethodError: no method matching +(::Grid1, ::Vector{Int64})
Closest candidates are:
+(::Any, ::Any, ::Any, ::Any...)
@ Base operators.jl:587
+(::Grid1, ::Int64)
@ Main In[27]:3
+(::Grid1, ::Integer)
@ Main In[26]:3
...
Autres surcharges#
Toutes les fonctions usuelles sont surchargeable sans limite : size(); det() …
function Base.size(g::AbstractGrid)
return g.n
end
size(a)
5
using LinearAlgebra
function LinearAlgebra.det(g::Grid1)
g.fin-g.debut
end
det(a)
1.0
Type et constructeurs#
Chaque langage “objet” définit un constructeur pour ces objets. Nous avons déjà utilisé un constructeur générique qui rempli chaque champ du nouveau type. Il est possible de faire une variante suivant le nombre d’arguments d’entrée et de leur type
abstract type AbstractGrid end # juste en dessous de Any
struct Grid1D <: AbstractGrid
debut::Float64
fin::Float64
n::Int32
# constructeurs par défaut sans argument
function Grid1D()
new(0,0,0)
end
# constructeurs par défaut avec argument
function Grid1D(a,b,c)
@assert c > 0 "Attention la taille de la grille est négative ou nulle"
new(a,b,c)
end
end
b = Grid1D(0, 1, -1)
AssertionError: Attention la taille de la grille est négative ou nulle
Stacktrace:
[1] Grid1D(a::Int64, b::Int64, c::Int64)
@ Main ./In[46]:15
[2] top-level scope
@ In[47]:1
Il devient possible de déterminer un constructeurs pour différentes entrées.
Il faut au préalable bien penser sa hiérarchie de type et écrire autant de fonctions constructeurs que de cas d’initialisation du nouveau type.
Les Itérateurs#
Il est possible sur un type nouveau de définir un itérateur, comme ici de parcourir les points de la grille, définissons (surchargeons) de nouvelles fonctions ou plutôt méthodes :
Base.iterate(g::Grid1D, state=g.debut) = begin
state > g.fin ? nothing : (state, state+(g.fin-g.debut)/g.n)
end
grid = Grid1D(0, 2 , 10)
Grid1D(0.0, 2.0, 10)
for x in grid
println(x)
end
0.0
0.2
0.4
0.6000000000000001
0.8
1.0
1.2
1.4
1.5999999999999999
1.7999999999999998
1.9999999999999998
Il devient possible de construire des itérateurs sur une grille 2d, 3d renvoyant les coordonnées des points de la grille… Mais on peut imaginer sur une triangulation etc…