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, Float64Julia 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)
# 2 methods for generic function f from Main:
  • 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…