17. Dispatch multiple▲
Julia offre la possibilité d'écrire du code capable de fonctionner sur différents types, ce qui est connu comme de la programmation générique. Dans ce chapitre, nous abordons l'utilisation des déclarations de type en Julia et nous présentons des méthodes permettant d'implémenter différents comportements pour une fonction selon les types associés à ses arguments. Il s'agit là du dispatch multiple (multiple dispatch).
17-1. Déclarations de types▲
L'opérateur ::
associe des annotations de type aux expressions et aux variables :
2.
3.
4.
julia>
(
1
+
2
)
::
Float64
ERROR: TypeError
: in
typeassert, expected Float64
, got Int64
julia>
(
1
+
2
)
::
Int64
3
Cela permet de confirmer qu'un programme fonctionne de manière adéquate.
Par ailleurs, l'opérateur ::
peut être ajouté dans le membre de gauche d'une affectation ou dans le cadre d'une déclaration.
2.
3.
4.
5.
6.
7.
8.
9.
julia>
function
returnfloat()
x::
Float64
=
100
x
end
returnfloat (
generic function
with 1
method)
julia>
x =
returnfloat()
100.0
julia>
typeof(
x)
Float64
En l'occurrence, la variable x est toujours de type Float64
et la valeur est convertie en virgule flottante si nécessaire.
Une annotation de type peut également être jointe à l'en-tête de la définition d'une fonction :
2.
3.
4.
5.
6.
function
sinc(
x)
::
Float64
if
x ==
0
return
1
end
sin(
x)
/
(
x)
end
La valeur de retour de sinc est toujours convertie en type Float64
.
Par défaut, lorsque les types ne sont pas précisés, Julia considère les valeurs comme étant de type quelconque (Any
).
17-2. Méthodes▲
Dans la figure 16.1.1, nous avons défini une structure appelée MyTime et dans la section 16.1Heures, minutes et secondes, nous avons écrit une fonction appelée printtime :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
using
Printf
struct
MyTime
heure ::
Int64
minute ::
Int64
seconde ::
Int64
end
function
printtime(
time)
@printf
(
"%02d:%02d:%02d"
, time.heure, time.minute, time.seconde)
end
Comme on peut le constater, les déclarations de type peuvent (et, pour des raisons de performance, devraient) être associées aux champs d'une définition de structure.
Pour appeler la fonction printtime, il est nécessaire de passer un objet MyTime en argument :
2.
3.
4.
julia>
start =
MyTime(
9
, 55
, 0
)
MyTime(
9
, 55
, 0
)
julia>
printtime(
start)
09
:55
:00
Pour ajouter à la fonction printtime une méthode qui n'accepte comme seul argument qu'un objet MyTime, il suffit d'ajouter ::
suivi de MyTime à l'argument time dans la définition de la fonction :
2.
3.
function
printtime(
time::
MyTime)
@printf
(
"%02d:%02d:%02d"
, time.heure, time.minute, time.seconde)
end
Une méthode est une définition de fonction avec une signature spécifique : printtime a un argument de type MyTime.
Appeler la fonction printtime avec un objet MyTime produit le même résultat :
2.
julia>
printtime(
start)
09
:55
:00
À présent, nous pouvons redéfinir la première méthode sans l'annotation de type ::
, ce qui permet l'usage d'un argument de type quelconque :
2.
3.
function
printtime(
time)
println(
"Je ne sais pas comment afficher l'argument time."
)
end
Si nous appelons la fonction printtime avec un objet différent de MyTime, nous obtenons :
2.
julia>
printtime(
150
)
Je ne sais pas comment afficher l'argument time.
17-2-1. Exercice 17-1▲
Réécrivez time et inttotime pour spécifier leur argument (voir la section 16.4Prototypage ou planification ?).
17-3. Exemples supplémentaires▲
Voici une version de la fonction increment (voir la section 16.3Modificateurs) réécrite pour spécifier ses arguments :
2.
3.
4.
function
increment(
time::
MyTime, secondes::
Int64
)
secondes +=
timetoint(
time)
inttotime(
secondes)
end
À présent, il s'agit d'une fonction pure et non plus d'un modificateur.
Voici comment cette fonction increment peut être invoquée :
2.
3.
4.
julia>
start =
MyTime(
9
, 45
, 0
)
MyTime(
9
, 45
, 0
)
julia>
increment(
start, 1337
)
MyTime(
10
, 7
, 17
)
Si les arguments apparaissent dans le mauvais ordre, Julia retourne une erreur :
2.
julia>
increment(
1337
, start)
ERROR: MethodError
: no method matching increment(
::
Int64
, ::
MyTime)
En effet, la signature de la méthode est increment(
time::
MyTime, seconds::
Int64
)
et non increment(
seconds::
Int64
, time::
MyTime)
.
Réécrire isafter pour agir uniquement sur les objets MyTime est tout aussi aisé :
2.
3.
function
isafter(
t1::
MyTime, t2::
MyTime)
(
t1.heure, t1.minute, t1.seconde)
>
(
t2.heure, t2.minute, t2.seconde)
end
Au fait, les arguments optionnels sont implémentés comme syntaxe pour les définitions de méthodes multiples. Par exemple, cette définition :
2.
3.
function
f(
a=
1
, b=
2
)
a +
2
b
end
se traduit par les trois méthodes suivantes :
2.
3.
f(
a, b)
=
a +
2
b
f(
a)
=
f(
a, 2
)
f()
=
f(
1
, 2
)
En Julia, ces expressions sont des définitions valides de méthode. Il s'agit d'une notation abrégée pour la définition des fonctions/méthodes.
17-4. Constructeurs▲
Un constructeur est une fonction spéciale qui est appelée pour créer un objet. Les méthodes de constructeur par défaut de MyTime ont les signatures suivantes :
2.
MyTime(
heure, minute, seconde)
MyTime(
heure::
Int64
, minute::
Int64
, seconde::
Int64
)
Nous pouvons également ajouter nos propres méthodes de construction externes :
2.
3.
function
MyTime(
time::
MyTime)
MyTime(
time.heure, time.minute, time.seconde)
end
Cette dernière méthode est appelée constructeur de copie, car le nouvel objet MyTime est une copie de son argument.
Pour forcer l'usage des invariants, il est nécessaire de recourir à la méthode des constructeurs internes :
2.
3.
4.
5.
6.
7.
8.
9.
10.
struct
MyTime
heure ::
Int64
minute ::
Int64
seconde ::
Int64
function
MyTime(
heure::
Int64
=
0
, minute::
Int64
=
0
, seconde::
Int64
=
0
)
@assert
(
0
≤ minute <
60
, "Minute n'est pas entre 0 et 60."
)
@assert
(
0
≤ seconde <
60
, "Seconde n'est pas entre 0 et 60."
)
new
(
heure, minute, seconde)
end
end
La structure MyTime dispose maintenant de quatre méthodes à constructeurs internes :
2.
3.
4.
MyTime()
MyTime(
heure::
Int64
)
MyTime(
heure::
Int64
, minute::
Int64
)
MyTime(
hour::
Int64
, minute::
Int64
, seconde::
Int64
)
Une méthode à constructeur interne est toujours définie à l'intérieur du bloc d'une déclaration de type. Elle a accès à une fonction spéciale appelée new
qui crée des objets du type nouvellement déclaré.
Le constructeur par défaut n'est pas disponible si un constructeur interne est défini. Il faut écrire explicitement tous les constructeurs internes dont on a besoin.
Une deuxième méthode avec la fonction locale new
sans arguments existe :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
mutable struct
MyTime
heure ::
Int
minute ::
Int
seconde ::
Int
function
MyTime(
heure::
Int64
=
0
, minute::
Int64
=
0
, seconde::
Int64
=
0
)
@assert
(
0
≤ minute <
60
, "Les minutes sont comprises entre 0 et 60."
)
@assert
(
0
≤ seconde <
60
, "Les secondes sont comprises entre 0 et 60."
)
time =
new
()
time.heure =
heure
time.minute =
minute
time.seconde =
seconde
time
end
end
Cela permet de construire des structures de données récursives, c'est-à-dire une structure dont un des champs est la structure elle-même. Dans ce cas, la structure doit être non persistante puisque ses champs sont modifiés après l'instanciation.
17-5. show▲
show est une fonction spéciale qui retourne une représentation en chaîne d'un objet. Par exemple, voici une méthode show pour les objets MyTime :
2.
3.
4.
5.
using
Printf
function
Base.show(
io::
IO, time::
MyTime)
@printf
(
io, "%02d:%02d:%02d"
, time.heure, time.minute, time.seconde)
end
Le préfixe Base
est nécessaire parce que nous voulons ajouter une nouvelle méthode à la fonction Base.show.
Lorsqu'un objet est affiché, Julia invoque la fonction show :
2.
julia>
time =
MyTime(
9
, 45
)
09
:45
:00
Lorsque nous écrivons un nouveau type composite, il est pertinent :
- de commencer presque toujours par écrire un constructeur extérieur (ceci facilite l'instanciation des objets) ;
- d'utiliser show (qui est utile pour le débogage).
17-5-1. Exercice 17-2▲
Écrivez une méthode de construction extérieure pour la classe Point qui prend x ainsi que y comme paramètres optionnels et les affecte aux champs correspondants.
17-6. Surcharge d'opérateurs▲
En définissant les méthodes des opérateurs, nous pouvons spécifier leur comportement sur des types définis par le programmeur. Par exemple, si nous définissons une méthode nommée +
avec deux arguments MyTime, nous pouvons utiliser l'opérateur +
capable d'additionner des objets MyTime.
Voici à quoi pourrait ressembler la définition :
2.
3.
4.
5.
6.
import
Base.+
function
+
(
t1::
MyTime, t2::
MyTime)
secondes =
timetoint(
t1)
+
timetoint(
t2)
inttotime(
secondes)
end
La déclaration d'importation ajoute l'opérateur +
au champ d'application local afin que des méthodes puissent être ajoutées.
Voici comment nous pouvons l'utiliser :
2.
3.
4.
5.
6.
julia>
start =
MyTime(
9
, 55
)
09
:55
:00
julia>
duration =
MyTime(
2
, 11
, 0
)
02
:11
:00
julia>
start +
duration
12
:06
:00
Lorsque l'opérateur +
est appliqué aux objets MyTime, Julia invoque la méthode nouvellement ajoutée. Lorsque le REPL affiche le résultat, Julia invoque show. Il se passe donc beaucoup de choses en coulisses.
L'ajout au comportement d'un opérateur pour qu'il fonctionne avec des types définis par le programmeur s'appelle la surcharge de l'opérateur.
17-7. Dispatch multiple▲
Dans la section précédente, nous avons additionné deux objets MyTime. Cependant, il est également possible d'ajouter un entier à un objet MyTime :
2.
3.
function
+
(
time::
MyTime, secondes::
Int64
)
increment(
time, secondes)
end
Voici un exemple qui utilise l'opérateur +
avec un objet MyTime et un entier :
2.
3.
4.
julia>
start =
MyTime(
9
, 55
)
09
:55
::
00
julia>
start +
1337
10
:07
:17
L'addition étant un opérateur commutatif, il faut donc ajouter une méthode complémentaire.
2.
3.
function
+
(
secondes::
Int64
, time::
MyTime)
time +
secondes
end
Nous obtenons alors le même résultat :
2.
julia>
1337
+
start
10
:07
:17
Le choix de la méthode à exécuter lorsqu'une fonction est appliquée s'appelle un dispatch. Julia permet au processus de dispatching de choisir la méthode d'une fonction à appeler selon le nombre d'arguments passés et les types de chacun des arguments de la fonction. L'utilisation de tous les arguments d'une fonction pour laisser le choix de la méthode à invoquer est connue sous le nom de dispatch multiple(44).
17-7-1. Exercice 17-3▲
Écrivez des méthodes +
pour les objets Point (voir le chapitre 15Structures et objets) :
- Si les deux opérandes sont des objets point, la méthode doit retourner un nouvel objet Point dont la coordonnée x est la somme des coordonnées x des opérandes, de même pour les coordonnées y ;
- Si le premier ou le second opérande est un tuple, la méthode doit ajouter le premier élément du tuple à la coordonnée x et le second élément à la coordonnée y, et retourner un nouvel objet point avec le résultat.
17-8. Programmation générique (généricité)▲
Lorsqu'il est nécessaire, le dispatch multiple est d'une grande utilité. Ce n'est malheureusement pas toujours le cas. Souvent, il est possible de l'éviter en écrivant des fonctions qui se comportent correctement pour des arguments de types différents.
De nombreuses fonctions que nous avons écrites pour des chaînes de caractères fonctionnent également pour d'autres types de séquences. Par exemple, dans la section 11.2Dictionnaires en tant que collections de compteurs, nous avons utilisé la fonction histogram pour compter le nombre d'occurrences de chaque lettre apparaissant dans un mot.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
function
histogram(
s)
d =
Dict()
for
c in
s
if
c ∉ keys(
d)
d[c] =
1
else
d[c] +=
1
end
end
d
end
Cette fonction agit également sur les tableaux, les tuples et même les dictionnaires à condition que les éléments de s soient hachables, afin qu'ils puissent être utilisés comme clés dans le dictionnaire d.
2.
3.
4.
5.
6.
7.
julia>
t =
(
"poêle"
, "œuf"
, "poêle"
, "poêle"
, "jambon"
, "poêle"
)
(
"poêle"
, "œuf"
, "poêle"
, "poêle"
, "poêle"
, "poêle"
)
julia>
histogram(
t)
Dict{
Any
,Any
}
with 3
entries :
"jambon"
=>
1
"poêle"
=>
4
"œuf"
=>
1
Les fonctions capables de manipuler plusieurs types sont dites polymorphiques. Le polymorphisme contribue à la réutilisation du code.
Par exemple, la fonction interne sum, qui ajoute les éléments d'une séquence, remplit son rôle tant que les éléments de la séquence supportent l'addition.
Comme une méthode +
est fournie pour les objets MyTime, ceux-ci fonctionnent avec sum :
2.
3.
4.
5.
6.
7.
8.
julia>
t1 =
MyTime(
1
, 7
, 2
)
01
:07
:02
julia>
t2 =
MyTime(
1
, 5
, 8
)
01
:05
:08
julia>
t3 =
MyTime(
1
, 5
, 0
)
01
:05
:00
julia>
sum((
t1, t2, t3))
03
:17
:10
En général, si toutes les opérations à l'intérieur d'une fonction remplissent leur rôle avec un type donné, la fonction fera de même avec ce type.
Le meilleur type de polymorphisme est le type involontaire, où vous découvrez qu'une fonction que vous avez déjà écrite peut être appliquée à un type jusque-là imprévu.
17-9. Interface et implémentation▲
Un des objectifs du dispatch multiple est de rendre les logiciels plus faciles à gérer et à entretenir. Cela signifie qu'il est possible d'une part de continuer à faire fonctionner le programme lorsque d'autres parties du système changent et, de l'autre, de modifier le programme pour répondre à de nouvelles exigences.
Un principe de conception qui contribue à atteindre cet objectif consiste à garder les interfaces séparées des implémentations. Autrement dit, les méthodes ayant un argument annoté avec un type ne doivent pas dépendre de la façon dont les champs de ce type sont représentés.
Par exemple, dans ce chapitre, nous avons développé une structure qui représente un moment de la journée. Les méthodes ayant un argument annoté avec ce type comprennent timetoint, isafter et +
.
Nous pourrions implémenter ces méthodes de plusieurs manières. Les détails de l'implémentation dépendent de la façon dont nous représentons MyTime. Dans ce chapitre, les champs d'un objet MyTime étaient l'heure, la minute et la seconde.
Comme autre option, nous aurions pu remplacer ces champs par un seul entier représentant le nombre de secondes depuis minuit. Cette implémentation rendrait certaines fonctions, comme isafter, plus faciles à écrire. En revanche, d'autres seraient plus difficiles à développer.
Après avoir déployé un nouveau type, vous pourriez découvrir une meilleure implémentation. Si d'autres parties du programme utilisent votre type, cela peut être chronophage et provoquer des erreurs par changement de l'interface.
Cependant, si vous avez conçu l'interface avec soin, vous pouvez modifier l'implémentation sans changer l'interface. La conséquence immédiate est que les autres parties du programme ne doivent pas être modifiées.
17-10. Débogage▲
Appeler une fonction avec les bons arguments peut être ardu lorsque plus d'une méthode est spécifiée pour la fonction. Julia permet d'effectuer une introspection des signatures associées aux méthodes d'une fonction.
Pour savoir quelles méthodes sont disponibles pour une fonction donnée, la fonction methods vient à point :
2.
3.
4.
julia>
methods(
printtime)
# 2 methods for generic function "printtime":
[1
] printtime(
time::
MyTime)
in
Main at REPL[3
]:2
[2
] printtime(
time)
in
Main at REPL[4
]::
2
17-11. Glossaire▲
annotation de type l'opérateur :: suivi d'un type indiquant qu'une expression ou une variable est de ce type.
méthode définition d'un comportement possible d'une fonction.
dispatch choix de la méthode à mettre en œuvre lorsqu'une fonction est exécutée.
signature c'est le nombre et le type des arguments d'une méthode permettant au dispatch de sélectionner la méthode la plus spécifique d'une fonction lors de l'appel de fonction.
constructeur externe constructeur défini en dehors de la définition de type pour spécifier les méthodes utiles à la création d'un objet.
constructeur interne constructeur défini à l'intérieur de la définition de type pour imposer des invariants ou pour construire des objets récursifs.
constructeur par défaut constructeur interne disponible lorsqu'aucun constructeur interne défini par le programmeur n'est fourni.
constructeur de copie méthode de construction extérieure d'un type avec comme seul argument un objet du type. Cette méthode crée un nouvel objet, une copie de l'argument.
surcharge d'opérateur extension du comportement d'un opérateur (comme +) pour que celui fonctionne avec un type défini par le programmeur.
dispatch multiple (multiméthode) dispatch basé sur l'ensemble des arguments d'une fonction.
programmation générique (généricité) rédaction d'un code susceptible d'opérer avec plusieurs types.
17-12. Exercices▲
17-12-1. Exercice 17-4▲
Modifiez les champs de MyTime pour qu'il s'agisse d'un seul nombre entier représentant les secondes depuis minuit. Ensuite, modifiez les méthodes définies dans ce chapitre pour qu'elles fonctionnent avec la nouvelle implémentation.
17-12-2. Exercice 17-5▲
Rédigez une définition pour un type nommé Kangaroo avec un champ nommé putinpocket du type Array
et les méthodes suivantes :
-
Un constructeur qui initialise pouchcontents dans un tableau vide.
- Une méthode nommée putinpouch qui prend un objet Kangaroo et un objet de n'importe quel type et l'ajoute à pouchcontents.
-
Une méthode show qui retourne une représentation en chaîne de caractères de l'objet Kangaroo et du contenu de la poche.
Testez votre code en créant deux objets Kangaroo, en les affectant à des variables nommées kanga et roo, puis en ajoutant roo au contenu de la poche de kanga.