La répartition multiple ou multiméthodes est une fonctionnalité de certains langages de programmation dans laquelle une fonction ou une méthode peut être répartie dynamiquement en fonction du type d'exécution (dynamique) ou, dans le cas plus général, d'un autre attribut de plusieurs de ses arguments . Il s'agit d'une généralisation du polymorphisme de répartition unique où un appel de fonction ou de méthode est réparti dynamiquement en fonction du type dérivé de l'objet sur lequel la méthode a été appelée. La répartition multiple achemine la répartition dynamique vers la fonction ou la méthode d'implémentation en utilisant les caractéristiques combinées d'un ou plusieurs arguments.
Comprendre la répartition
Les développeurs de logiciels informatiques organisent généralement le code source en blocs nommés de diverses manières : sous-routines , procédures, sous-programmes, fonctions ou méthodes. Le code de la fonction est exécuté en l'appelant , c'est-à-dire en exécutant un morceau de code qui fait référence à son nom . Cela transfère temporairement le contrôle à la fonction appelée ; lorsque l'exécution de la fonction est terminée, le contrôle est généralement retransféré à l'instruction de l' appelant qui suit la référence.
Les noms de fonction sont généralement choisis de manière à décrire l'objectif de la fonction. Il est parfois souhaitable de donner le même nom à plusieurs fonctions, souvent parce qu'elles effectuent des tâches conceptuellement similaires, mais opèrent sur des types de données d'entrée différents. Dans de tels cas, la référence de nom au site d'appel de fonction n'est pas suffisante pour identifier le bloc de code à exécuter. Au lieu de cela, le nombre et le type des arguments de l'appel de fonction sont également utilisés pour sélectionner parmi plusieurs implémentations de fonction.
Dans les langages de programmation orientés objet plus conventionnels, c'est- à-dire à répartition unique , lors de l'appel d'une méthode ( envoi d'un message en Smalltalk , appel d'une fonction membre en C++ ), l'un de ses arguments est traité de manière spéciale et utilisé pour déterminer laquelle des classes (potentiellement nombreuses) de méthodes de ce nom doit être appliquée. Dans de nombreux langages, l' argument spécial est indiqué syntaxiquement ; par exemple, un certain nombre de langages de programmation placent l'argument spécial avant un point lors d'un appel de méthode : special.method(other, arguments, here), ce qui lion.sound()produirait un rugissement, alors que sparrow.sound()produirait un pépiement.
En revanche, dans les langages à répartition multiple, la méthode sélectionnée est simplement celle dont les arguments correspondent au nombre et au type de l'appel de fonction. Il n'existe aucun argument spécial qui possède la fonction/méthode exécutée dans un appel particulier.
Le Common Lisp Object System (CLOS) est un exemple ancien et bien connu de répartition multiple. Un autre exemple notable de l'utilisation de la répartition multiple est le langage de programmation Julia .
La répartition multiple doit être distinguée de la surcharge de fonctions , dans laquelle des informations de typage statiques, telles que le type déclaré ou inféré d'un terme (ou le type de base dans un langage avec sous-typage) sont utilisées pour déterminer laquelle des nombreuses possibilités sera utilisée sur un site d'appel donné, et cette détermination est effectuée au moment de la compilation ou de la liaison (ou à un autre moment avant le début de l'exécution du programme) et est ensuite invariante pour un déploiement ou une exécution donnée du programme. De nombreux langages tels que C++ offrent une surcharge de fonctions robuste mais n'offrent pas de répartition multiple dynamique (C++ ne permet la répartition unique dynamique que par l'utilisation de fonctions virtuelles).
Types de données
Lorsque vous travaillez avec des langages capables de distinguer les types de données au moment de la compilation , une sélection parmi les alternatives peut alors se produire. L'acte de créer de telles fonctions alternatives pour la sélection au moment de la compilation est généralement appelé surcharge d'une fonction.
Dans les langages de programmation qui diffèrent l'identification du type de données jusqu'au moment de l'exécution (c'est-à-dire la liaison tardive ), la sélection entre les fonctions alternatives doit alors avoir lieu, en fonction des types d'arguments de fonction déterminés dynamiquement. Les fonctions dont les implémentations alternatives sont sélectionnées de cette manière sont généralement appelées multiméthodes .
La répartition dynamique des appels de fonction entraîne un certain coût d'exécution. Dans certains langages, la distinction entre surcharge et multiméthodes peut être floue, le compilateur déterminant si la sélection au moment de la compilation peut être appliquée à un appel de fonction donné, ou si une répartition plus lente au moment de l'exécution est nécessaire.
Problèmes
Il existe plusieurs problèmes connus avec la répartition dynamique, qu'elle soit unique ou multiple. Bien que la plupart de ces problèmes soient résolus pour la répartition unique, qui est une fonctionnalité standard des langages de programmation orientés objet depuis des décennies, ces problèmes deviennent plus complexes dans le cas de la répartition multiple.
Expressivité et modularité
Dans la plupart des langages de programmation les plus courants, le code source est livré et déployé sous forme de granules de fonctionnalités que nous appellerons ici packages ; la terminologie utilisée pour ce concept varie selon les langages. Chaque package peut contenir plusieurs définitions de type, de valeur et de fonction. Les packages sont souvent compilés séparément dans les langages avec une étape de compilation et une relation de dépendance non cyclique peut exister. Un programme complet est un ensemble de packages, avec un package principal qui peut dépendre de plusieurs autres packages, et le programme entier consistant en la clôture transitive de la relation de dépendance.
Le problème d'expression concerne la capacité du code d'un package dépendant à étendre les comportements (fonctions ou types de données) définis dans un package de base à partir d'un package inclus, sans modifier la source du package de base. Les langages OO traditionnels à répartition unique rendent trivial l'ajout de nouveaux types de données mais pas de nouvelles fonctions ; les langages fonctionnels traditionnels ont tendance à avoir l'effet inverse, et la répartition multiple, si elle est implémentée correctement, permet les deux. Il est souhaitable qu'une implémentation de répartition multiple ait les propriétés suivantes :
- Il est possible de définir différents « cas » d'une multi-méthode à partir de différents packages sans modifier la source d'un package de base.
- L'inclusion d'un autre package dans le programme ne doit pas modifier le comportement d'un appel multi-méthode donné, lorsque l'appel n'utilise aucun type de données défini dans le package.
- Inversement, si un type de données est défini dans un package donné, et qu'une extension multi-méthode utilisant ce type est également définie dans le même package, et qu'une valeur de ce type est transmise (via une référence de type de base ou dans une fonction générique) dans un autre package sans dépendance sur ce package, et que la multi-méthode est ensuite invoquée avec cette valeur comme argument, le cas multi-méthode défini dans le package qui inclut le type doit être utilisé. En d'autres termes, dans un programme donné, la même multi-méthode invoquée avec le même ensemble d'arguments doit se résoudre en la même implémentation, quel que soit l'emplacement du site d'appel, et qu'une définition donnée soit ou non « dans la portée » ou « visible » au point d'appel de la méthode.
Ambiguïté
Il est généralement souhaitable que pour toute invocation donnée d'une méthode multiple, il y ait au plus un « meilleur » candidat parmi les cas d'implémentation de la méthode multiple, et/ou que s'il n'y en a pas, cela soit résolu de manière prévisible et déterministe, y compris l'échec. Un comportement non déterministe est indésirable. En supposant un ensemble de types avec une relation de sous-typage non circulaire, on peut définir qu'une implémentation d'une méthode multiple est « meilleure » (plus spécifique) si tous les arguments envoyés dynamiquement dans la première sont des sous-types de tous les arguments envoyés dynamiquement spécifiés dans la seconde, et au moins un est un sous-type strict. Avec une expédition unique et en l'absence d' héritage multiple , cette condition est trivialement satisfaite, mais avec une expédition multiple, il est possible que deux candidats ou plus satisfassent une liste d'arguments réelle donnée, mais aucun n'est plus spécifique que l'autre (un argument dynamique étant le sous-type dans un cas, un autre étant le sous-type dans l'autre cas). Cela peut se produire en particulier si deux packages différents, ne dépendant pas l'un de l'autre, étendent tous deux une méthode multiple avec des implémentations concernant les types de chaque package, puis qu'un troisième package qui inclut les deux (éventuellement indirectement) invoque ensuite la méthode multiple en utilisant des arguments des deux packages.
Les résolutions possibles incluent :
- Traiter tous les appels ambigus comme une erreur. Cela peut être détecté au moment de la compilation (ou avant le déploiement), mais peut ne pas être détecté avant l'exécution et produire une erreur d'exécution.
- Ordonner les arguments, par exemple, le cas avec le premier argument le plus spécifique est sélectionné, et les arguments suivants ne sont pas pris en compte pour la résolution d'ambiguïté, à moins que le premier argument ne soit insuffisant pour résoudre le problème.
- Construction d'autres règles pour résoudre une ambiguïté dans un sens ou dans l'autre. Parfois, de telles règles peuvent être arbitraires et surprenantes. Dans les règles de résolution de surcharge statique en C++, par exemple, un type qui correspond exactement est naturellement considéré comme une meilleure correspondance qu'un type qui correspond via une référence de type de base ou un paramètre générique (modèle). Cependant, si les seules correspondances possibles se font via un type de base ou un paramètre générique, le paramètre générique est préféré au type de base, une règle qui produit parfois un comportement surprenant.
Efficacité
L'implémentation efficace de l'envoi unique, y compris dans les langages de programmation compilés séparément en code objet et liés avec un éditeur de liens de bas niveau (non sensible au langage), y compris de manière dynamique au moment du chargement/démarrage du programme ou même sous la direction du code de l'application, est bien connue. La méthode « vtable » développée en C++ et dans d'autres langages OO précoces (où chaque classe possède un tableau de pointeurs de fonction correspondant aux fonctions virtuelles de cette classe) est presque aussi rapide qu'un appel de méthode statique, nécessitant une surcharge O(1) et une seule recherche de mémoire supplémentaire même dans le cas non optimisé. Cependant, la méthode vtable utilise le nom de la fonction et non le type d'argument comme clé de recherche, et ne s'adapte pas au cas d'envoi multiple. (Cela dépend également du paradigme orienté objet selon lequel les méthodes sont des caractéristiques de classes, et non des entités autonomes indépendantes de tout type de données particulier).
La mise en œuvre efficace de l’envoi multiple reste un problème de recherche permanent.
Utilisation en pratique
Pour estimer la fréquence à laquelle la répartition multiple est utilisée dans la pratique, Muschevici et al. ont étudié des programmes qui utilisent la répartition dynamique. Ils ont analysé neuf applications, principalement des compilateurs, écrites dans six langages différents : Common Lisp Object System , Dylan , Cecil , MultiJava, Diesel et Nice. Leurs résultats montrent que 13 à 32 % des fonctions génériques utilisent le type dynamique d'un argument, tandis que 2,7 à 6,5 % d'entre elles utilisent le type dynamique de plusieurs arguments. Les 65 à 93 % restants des fonctions génériques ont une méthode concrète (overrider) et ne sont donc pas considérées comme utilisant les types dynamiques de leurs arguments. De plus, l'étude indique que 2 à 20 % des fonctions génériques en avaient deux et 3 à 6 % en avaient trois. Les chiffres diminuent rapidement pour les fonctions avec des overriders plus concrets.
Le dispatch multiple est beaucoup plus utilisé dans Julia , où le dispatch multiple était un concept de conception central depuis l'origine du langage : en collectant les mêmes statistiques que Muschevici sur le nombre moyen de méthodes par fonction générique, il a été constaté que la bibliothèque standard Julia utilise plus du double de la quantité de surcharge que dans les autres langages analysés par Muschevici, et plus de 10 fois dans le cas des opérateurs binaires .
Les données de ces articles sont résumées dans le tableau suivant, où le ratio de répartition DRest le nombre moyen de méthodes par fonction générique ; le ratio de choix CRest la moyenne du carré du nombre de méthodes (pour mieux mesurer la fréquence des fonctions avec un grand nombre de méthodes) ; et le degré de spécialisation DoSest le nombre moyen d'arguments spécialisés en type par méthode (c'est-à-dire le nombre d'arguments qui sont répartis sur) :
Théorie
La théorie des langages de répartition multiples a été développée pour la première fois par Castagna et al., en définissant un modèle pour les fonctions surchargées avec liaison tardive . Il a donné lieu à la première formalisation du problème de covariance et de contravariance des langages orientés objet et à une solution au problème des méthodes binaires.
Exemples
La distinction entre les envois multiples et les envois uniques peut être rendue plus claire par un exemple. Imaginez un jeu qui a, parmi ses objets (visibles par l'utilisateur), des vaisseaux spatiaux et des astéroïdes. Lorsque deux objets entrent en collision, le programme peut avoir besoin de faire des choses différentes selon ce qui vient de frapper quoi.
Langues avec répartition multiple intégrée
C#
C# a introduit la prise en charge des multiméthodes dynamiques dans la version 4 (avril 2010) à l'aide du mot-clé « dynamic ». L'exemple suivant illustre les multiméthodes. Comme de nombreux autres langages à typage statique, C# prend également en charge la surcharge de méthodes statiques. Microsoft s'attend à ce que les développeurs choisissent le typage statique plutôt que le typage dynamique dans la plupart des scénarios. Le mot-clé « dynamic » prend en charge l'interopérabilité avec les objets COM et les langages .NET à typage dynamique.
L'exemple ci-dessous utilise des fonctionnalités introduites dans C# 9 et C# 10.
en utilisant ColliderLibrary statique ; Console . WriteLine ( Collide ( nouvel astéroïde ( 101 ), nouveau vaisseau spatial ( 300 ))); Console . WriteLine ( Collide ( nouvel astéroïde ( 10 ), nouveau vaisseau spatial ( 10 ))); Console . WriteLine ( Collide ( nouveau vaisseau spatial ( 101 ), nouveau vaisseau spatial ( 10 ))); chaîne Collide ( SpaceObject x , SpaceObject y ) => x . Taille > 100 && y . Taille > 100 ? "Big boom!" : CollideWith ( x comme dynamique , y comme dynamique ); // Envoi dynamique à la méthode CollideWith classe ColliderLibrary { public static string CollideWith ( Astéroïde x , Astéroïde y ) => "a/a" ; public static string CollideWith ( Astéroïde x , Vaisseau spatial y ) => "a/s" ; public static string CollideWith ( Vaisseau spatial x , Astéroïde y ) => "s/a" ; public static string CollideWith ( Vaisseau spatial x , Vaisseau spatial y ) => "s/s" ; } enregistrement abstrait SpaceObject ( int Size ); enregistrement Asteroid ( int Size ) : SpaceObject ( Size ); enregistrement Spaceship ( int Size ) : SpaceObject ( Size );
Sortir:
Gros boum ! a/s s/s
Groovy
Groovy est un langage JVM compatible Java /interutilisable à usage général qui, contrairement à Java, utilise la liaison tardive/la répartition multiple.
/* L'implémentation Groovy de l'exemple C# ci-dessus La liaison tardive fonctionne de la même manière lors de l'utilisation de méthodes non statiques ou de la compilation statique de classes/méthodes (annotation @CompileStatic) */ class Program { static void main ( String [] args ) { println Collider . collide ( new Asteroid ( 101 ), new Spaceship ( 300 )) println Collider . collide ( new Asteroid ( 10 ), new Spaceship ( 10 )) println Collider . collide ( new Spaceship ( 101 ), new Spaceship ( 10 )) } } classe Collider { static String collide ( SpaceObject x , SpaceObject y ) { ( x . size > 100 && y . size > 100 ) ? "big-boom" : collideWith ( x , y ) // Envoi dynamique vers la méthode collideWith } Chaîne statique privée collideWith ( Astéroïde x , Astéroïde y ) { "a/a" } Chaîne statique privée collideWith ( Astéroïde x , Vaisseau spatial y ) { "a/s" } Chaîne statique privée collideWith ( Vaisseau spatial x , Astéroïde y ) { "s/a" } Chaîne statique privée collideWith ( Vaisseau spatial x , Vaisseau spatial y ) { "s/s" } } classe SpaceObject { int taille SpaceObject ( int taille ) { this . size = taille } } @InheritConstructors classe Astéroïde étend SpaceObject {} @InheritConstructors classe Vaisseau spatial étend SpaceObject {}
Lisp commun
Dans un langage avec répartition multiple, comme Common Lisp , cela pourrait ressembler davantage à ceci (exemple Common Lisp illustré) :
( defmethod collide-with (( x astéroïde ) ( y astéroïde )) ;; gérer un astéroïde percutant un astéroïde ) ( defmethod collide-with (( x astéroïde ) ( y vaisseau spatial )) ;; gérer un astéroïde percutant un vaisseau spatial ) ( defmethod collide-with (( x vaisseau spatial ) ( y astéroïde )) ;; gérer un vaisseau spatial percutant un astéroïde ) ( defmethod collide-with (( x vaisseau spatial ) ( y vaisseau spatial )) ;; gérer un vaisseau spatial percutant un vaisseau spatial )
et de même pour les autres méthodes. Les tests explicites et le « casting dynamique » ne sont pas utilisés.
En présence d'une répartition multiple, l'idée traditionnelle selon laquelle les méthodes sont définies dans des classes et contenues dans des objets devient moins attrayante : chaque méthode en collision ci-dessus est attachée à deux classes différentes, et non à une seule. Par conséquent, la syntaxe spéciale pour l'invocation de méthode disparaît généralement, de sorte que l'invocation de méthode ressemble exactement à l'invocation de fonction ordinaire, et les méthodes sont regroupées non pas dans des classes mais dans des fonctions génériques .
Julia
Julia a intégré la répartition multiple, et elle est au cœur de la conception du langage. La version Julia de l'exemple ci-dessus pourrait ressembler à :
type abstrait SpaceObject fin struct Astéroïde <: SpaceObject taille :: Int fin struct Vaisseau spatial <: SpaceObject taille :: Int fin collide_with ( :: Astéroïde , :: Vaisseau spatial ) = "a/s" collide_with ( :: Vaisseau spatial , :: Astéroïde ) = "s/a" collide_with ( :: Vaisseau spatial , :: Vaisseau spatial ) = "s/s" collide_with ( :: Astéroïde , :: Astéroïde ) = "a/a" collision ( x :: SpaceObject , y :: SpaceObject ) = ( x . taille > 100 && y . taille > 100 ) ? "Grand boum !" : collide_with ( x , y )
Sortir:
julia> collision ( astéroïde ( 101 ), vaisseau spatial ( 300 )) "Grand boum !" julia> collision ( astéroïde ( 10 ), vaisseau spatial ( 10 )) "a/s" julia> collision ( Vaisseau spatial ( 101 ), Vaisseau spatial ( 10 )) "s/s"
Raku
Raku , comme Perl, utilise des idées éprouvées issues d'autres langages, et les systèmes de types se sont révélés offrir des avantages convaincants dans l'analyse de code côté compilateur et une sémantique puissante côté utilisateur via la répartition multiple.
Il dispose à la fois de multiméthodes et de multisubs. Étant donné que la plupart des opérateurs sont des sous-routines, il dispose également de plusieurs opérateurs répartis.
En plus des contraintes de type habituelles, il dispose également de contraintes where qui permettent de créer des sous-routines très spécialisées.
sous-ensemble Masse de Réel où 0 ^..^ Inf ; rôle Stellar-Object { a une masse $.mass est requis ; nom de la méthode () renvoie Str {...}; } classe Asteroid fait Stellar-Object { nom de la méthode () { 'un astéroïde' } } classe Spaceship fait Stellar-Object { a Str $.name = 'un vaisseau spatial sans nom' ; } mon Str @destroyed = < oblitéré détruit mutilé >; mon Str @damaged = « endommagé 'est entré en collision avec' 'a été endommagé par' »; # Nous ajoutons des candidats multiples aux opérateurs de comparaison numérique car nous les comparons numériquement, # mais cela n'a aucun sens de contraindre les objets à un type numérique. # (S'ils le faisaient, nous n'aurions pas nécessairement besoin d'ajouter ces opérateurs.) # Nous aurions également pu définir des opérateurs entièrement nouveaux de la même manière. multi sub infix: « <=> » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . mass <=> $b . mass } multi sub infix: « < » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . mass < $b . mass } multi sub infix: « > » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . mass > $b . mass } multi sub infix: « == » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . masse == $b . masse } # Définissez un nouveau répartiteur multiple et ajoutez des contraintes de type aux paramètres. # Si nous ne l'avions pas défini, nous aurions obtenu un répartiteur générique qui n'aurait pas de contraintes. proto sub collide ( Stellar-Object:D $, Stellar-Object:D $ ) {*} # Pas besoin de répéter les types ici car ils sont les mêmes que le prototype. # La contrainte 'where' s'applique techniquement uniquement à $b et non à l'ensemble de la signature. # Notez que la contrainte 'where' utilise l'opérateur candidat `<` que nous avons ajouté précédemment. multi sub collide ( $a , $b where $a < $b ) { say "$a.name() a été @destroyed.pick() par $b.name()" ; } multi sub collide ( $a , $b où $a > $b ) { # redispatcher vers le candidat précédent avec les arguments échangés samewith $b , $a ; } # Cela doit être après les deux premiers car les autres # ont des contraintes 'where', qui sont vérifiées dans l' ordre # dans lequel les sous-programmes ont été écrits. (Celui-ci correspondrait toujours.) multi sub collide ( $a , $b ) { # randomiser l'ordre my ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 @damaged.pick() $n2" ; } # Les deux candidats suivants peuvent être n'importe où après le proto, # car ils ont des types plus spécialisés que les trois précédents. # Si les vaisseaux ont une masse inégale, l'un des deux premiers candidats est appelé à la place. multi sub collide ( Spaceship $a , Spaceship $b where $a == $b ){ my ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 est entré en collision avec $n2, et les deux vaisseaux ont été " , ( @destroyed . pick , 'laissé endommagé' ). pick ; } # Vous pouvez décompresser les attributs dans des variables au sein de la signature. # Vous pourriez même avoir une contrainte sur eux `(:mass($a) where 10)`. multi sub collide ( Asteroid $ (: mass ( $a )), Asteroid $ (: mass ( $b )) ){ say "deux astéroïdes sont entrés en collision et se sont combinés en un seul astéroïde plus grand de masse { $a + $b }" ; } mon vaisseau spatial $Enterprise .= new (: mass ( 1 ),: name ( 'The Enterprise' )); collision Astéroïde . new (: mass ( .1 )), $Enterprise ; collision $Enterprise , vaisseau spatial . new (: mass ( .1 )); collision $Enterprise , astéroïde . new ( : mass ( 1 ) ) ; collision $ Enterprise , vaisseau spatial . new ( : mass ( 1 ) ) ; collision Astéroïde . new ( : mass ( 10 ) ) , astéroïde . new (: mass ( 5 ));
Extension des langages avec des bibliothèques à répartition multiple
JavaScript
Dans les langages qui ne prennent pas en charge la répartition multiple au niveau de la définition du langage ou de la syntaxe, il est souvent possible d'ajouter une répartition multiple à l'aide d'une extension de bibliothèque . JavaScript et TypeScript ne prennent pas en charge les méthodes multiples au niveau de la syntaxe, mais il est possible d'ajouter une répartition multiple via une bibliothèque. Par exemple, le package multimethod fournit une implémentation de fonctions génériques de répartition multiple.
Version typée dynamiquement en JavaScript :
importer { multi , méthode } depuis '@arrows/multimethod' classe Astéroïde {} classe Vaisseau spatial {} const collideWith = multi ( method ([ Astéroïde , Astéroïde ], ( x , y ) => { // gérer le cas d'un astéroïde percutant un astéroïde }), method ( [ Astéroïde , Vaisseau spatial ], ( x , y ) => { // gérer le cas d'un astéroïde percutant un vaisseau spatial } ), method ( [ Vaisseau spatial , Astéroïde ] , ( x , y ) = > { // gérer le cas d' un vaisseau spatial percutant un vaisseau spatial } ) , )
Version typée statiquement en TypeScript :
importer { multi , méthode , Multi } depuis '@arrows/multimethod' classe Astéroïde {} classe Vaisseau spatial {} type CollideWith = Multi & { ( x : Astéroïde , y : Astéroïde ) : void ( x : Astéroïde , y : Vaisseau spatial ) : void ( x : Vaisseau spatial , y : Astéroïde ) : void ( x : Vaisseau spatial , y : Vaisseau spatial ) : void } const collideWith : CollideWith = multi ( method ([ Astéroïde , Astéroïde ], ( x , y ) => { // gérer le cas d'un astéroïde percutant un astéroïde }), method ([ Astéroïde , Vaisseau spatial ], ( x , y ) = > { // gérer le cas d' un astéroïde percutant un vaisseau spatial } ), method ( [ Vaisseau spatial , Astéroïde ] , ( x , y ) = > { // gérer le cas d' un vaisseau spatial percutant un vaisseau spatial }) , )
Python
Il est possible d'ajouter plusieurs méthodes à Python à l'aide d'une extension de bibliothèque . Par exemple, en utilisant le module multimethod.py et également avec le module multimethods.py qui fournit des méthodes multiples de style CLOS pour Python sans modifier la syntaxe sous-jacente ou les mots-clés du langage.
à partir de multimethods import Dispatch à partir de game_objects import Asteroid , Spaceship à partir de game_behaviors import as_func , ss_func , sa_func collide = Dispatch () collide.add_rule ( ( Astéroïde , Vaisseau spatial) , as_func ) collide.add_rule ( ( Vaisseau spatial , Vaisseau spatial ), ss_func ) collide.add_rule ( ( Vaisseau spatial , Astéroïde ) , sa_func ) def aa_func ( a , b ): Comportement lorsqu'un astéroïde heurte un astéroïde.""" # ...définir un nouveau comportement... entrer en collision . add_rule (( Astéroïde , Astéroïde ), aa_func )
# ...plus tard... entrer en collision ( chose1 , chose2 )
Fonctionnellement, cela est très similaire à l'exemple CLOS, mais la syntaxe est celle de Python conventionnel.
En utilisant les décorateurs Python 2.4 , Guido van Rossum a produit un exemple d'implémentation de multiméthodes avec une syntaxe simplifiée :
@multimethod ( Asteroid , Asteroid ) def collide ( a , b ): Comportement lorsqu'un astéroïde heurte un astéroïde.""" # ...définir un nouveau comportement... @multimethod ( Asteroid , Spaceship ) def collide ( a , b ): Comportement lorsqu'un astéroïde heurte un vaisseau spatial.""" # ...définir un nouveau comportement... # ... définir d'autres règles multiméthodes ...
et ensuite il continue à définir le décorateur multiméthode.
Le package PEAK-Rules fournit plusieurs répartitions avec une syntaxe similaire à l'exemple ci-dessus. Il a ensuite été remplacé par PyProtocols.
La bibliothèque Reg prend également en charge la répartition multiple et prédicat.
Avec l'introduction des indications de type , la répartition multiple est possible avec une syntaxe encore plus simple. Par exemple, en utilisant plum-dispatch,
de l' expédition d'importation de prunes @dispatch def collide ( a : Asteroid , b : Asteroid ): Comportement lorsqu'un astéroïde heurte un astéroïde.""" # ...définir un nouveau comportement... @dispatch def collide ( a : Astéroïde , b : Vaisseau spatial ): Comportement lorsqu'un astéroïde heurte un vaisseau spatial.""" # ...définir un nouveau comportement... # ...définir d'autres règles...
Émulation de plusieurs envois
C
Le C ne dispose pas de répartition dynamique, il doit donc être implémenté manuellement sous une forme ou une autre. Souvent, une énumération est utilisée pour identifier le sous-type d'un objet. La répartition dynamique peut être effectuée en recherchant cette valeur dans une table de branches de pointeurs de fonction . Voici un exemple simple en C :
typedef void ( * CollisionCase )( void ); void collision_AA ( void ) { /* gérer les collisions astéroïdes-astéroïdes */ }; void collision_AS ( void ) { /* gérer les collisions astéroïdes-vaisseaux spatiaux */ }; void collision_SA ( void ) { /* gérer les collisions vaisseaux spatiaux-astéroïdes */ }; void collision_SS ( void ) { /* gérer les collisions vaisseaux spatiaux-vaisseaux spatiaux*/ }; typedef enum { THING_ASTEROID = 0 , THING_SPACESHIP , THING_COUNT /* pas un type de chose en soi, mais utilisé pour trouver le nombre de choses */ } Chose ; CollisionCase collisionCases [ THING_COUNT ][ THING_COUNT ] = { { & collision_AA , & collision_AS }, { & collision_SA , & collision_SS } }; void collision ( Chose a , Chose b ) { ( * collisionCases [ a ][ b ])(); } int main ( void ) { collision ( CHOSE_VAISSEAU_SPATIAL , CHOSE_ASTEROIDE ); }
Avec la bibliothèque C Object System, C prend en charge la répartition dynamique similaire à CLOS. Il est entièrement extensible et ne nécessite aucune manipulation manuelle des méthodes. Les messages dynamiques (méthodes) sont répartis par le répartiteur de COS, qui est plus rapide qu'Objective-C. Voici un exemple dans COS :
#include <stdio.h> #include <cos/Object.h> #include <cos/gen/object.h> // classes defclass ( Asteroid ) // membres de données endclass defclass ( Spaceship ) // membres de données endclass // génériques defgeneric ( _Bool , collision_avec , _1 , _2 ); // multiméthodes defmethod ( _Bool , collide_with , Asteroid , Asteroid ) // gérer l'astéroïde qui frappe un astéroïde endmethod defmethod ( _Bool , collide_with , Asteroid , Spaceship ) // gérer l'astéroïde frappant le vaisseau spatial endmethod defmethod ( _Bool , collide_with , Spaceship , Asteroid ) // gérer le vaisseau spatial heurtant un astéroïde endmethod defmethod ( _Bool , collide_with , Spaceship , Spaceship ) // gérer le vaisseau spatial qui heurte le vaisseau spatial endmethod // exemple d'utilisation int main ( void ) { OBJ a = gnew ( Astéroïde ); OBJ s = gnew ( Vaisseau spatial ); printf ( "<a,a> = %d " , collision_avec ( a , a )); printf ( "<a,s> = %d " , collision_avec ( a , s )); printf ( "<s,a> = %d " , collision_avec ( s , a )); printf ( "<s,s> = %d " , collision_avec ( s , s )); grelease ( a ); grelease ( s ); }
C++
En 2021 , C++ ne prend en charge nativement que la répartition unique, bien que l'ajout de méthodes multiples (répartition multiple) ait été proposé par Bjarne Stroustrup (et ses collaborateurs) en 2007. Les méthodes permettant de contourner cette limite sont analogues : utilisez soit le modèle de visiteur , soit le casting dynamique, soit une bibliothèque :
// Exemple utilisant la comparaison de type au moment de l'exécution via dynamic_cast struct Chose { void virtuel collideWith ( Chose & autre ) = 0 ; }; struct Asteroid : Thing { void collideWith ( Thing & other ) { // dynamic_cast vers un type pointeur renvoie NULL si le cast échoue // (dynamic_cast vers un type référence lèverait une exception en cas d'échec) if ( auto asteroid = dynamic_cast < Asteroid *> ( & other )) { // gérer les collisions astéroïde-astéroïde } else if ( auto spaceship = dynamic_cast < Spaceship *> ( & other )) { // gérer les collisions astéroïde-vaisseau spatial } else { // gestion des collisions par défaut ici } } }; struct Vaisseau spatial : Chose { void collideWith ( Chose & autre ) { if ( auto asteroid = dynamic_cast < Astéroïde *> ( & autre )) { // gérer les collisions Vaisseau spatial-Astéroïde } else if ( auto spaceship = dynamic_cast < Vaisseau spatial *> ( & autre )) { // gérer les collisions Vaisseau spatial-Vaisseau spatial } else { // gestion des collisions par défaut ici } } };
ou table de recherche de pointeur vers méthode :
#include <cstdint> #include <typeinfo> #include <unordered_map> classe Chose { protégée : Chose ( std :: uint32_t cid ) : tid ( cid ) {} const std :: uint32_t tid ; // type id typedef void ( Chose ::* CollisionHandler )( Chose et autre ); typedef std :: unordered_map < std :: uint64_t , CollisionHandler > CollisionHandlerMap ; static void addHandler ( std :: uint32_t id1 , std :: uint32_t id2 , CollisionHandler gestionnaire ) { collisionCases.insert ( CollisionHandlerMap :: value_type ( clé ( id1 , id2 ) , gestionnaire )) ; } static std :: uint64_t key ( std :: uint32_t id1 , std :: uint32_t id2 ) { return std :: uint64_t ( id1 ) << 32 | id2 ; } statique CollisionHandlerMap collisionCases ; public : void collideWith ( Thing & other ) { auto handler = collisionCases . find ( key ( tid , other . tid )); if ( handler != collisionCases . end ()) { ( this ->* handler -> second )( other ); // appel de pointeur vers une méthode } else { // gestion des collisions par défaut } } }; classe Astéroïde : public Chose { void asteroid_collision ( Chose & autre ) { /* gérer les collisions astéroïde-astéroïde */ } void spaceship_collision ( Chose & autre ) { /* gérer les collisions astéroïde-vaisseau spatial */ } public : Astéroïde () : Chose ( cid ) {} static void initCases (); static const std :: uint32_t cid ; }; classe Vaisseau spatial : public Thing { void asteroid_collision ( Thing & other ) { /*gère la collision Vaisseau spatial-Astéroïde*/ } void spaceship_collision ( Thing & other ) { /*gère la collision Vaisseau spatial-Vaisseau spatial*/ } public : Vaisseau spatial () : Chose ( cid ) {} static void initCases (); static const std :: uint32_t cid ; // identifiant de classe }; Chose :: CollisionHandlerMap Chose :: collisionCases ; const std :: uint32_t Astéroïde :: cid = typeid ( Astéroïde ) . hash_code () ; const std :: uint32_t Vaisseau spatial :: cid = typeid ( Vaisseau spatial ). hash_code () ; void Asteroid::initCases () { addHandler ( cid , cid , CollisionHandler ( & Asteroid :: asteroid_collision )); addHandler ( cid , Spaceship :: cid , CollisionHandler ( & Asteroid :: spaceship_collision )); } void Spaceship::initCases () { addHandler ( cid , Astéroïde :: cid , CollisionHandler ( & Spaceship :: astéroïde_collision )); addHandler ( cid , cid , CollisionHandler ( & Spaceship :: spaceship_collision )); } int main () { Astéroïde :: initCases (); Vaisseau spatial :: initCases (); Astéroïde a1 , a2 ; Vaisseau spatial s1 , s2 ; a1 . collideWith ( a2 ); a1 . collideWith ( s1 ); s1 .collideWith ( s2 ) ; s1 .collideWith ( a1 ) ; }
La bibliothèque YOMM2 fournit une implémentation rapide et orthogonale de multiméthodes ouvertes.
La syntaxe de déclaration des méthodes ouvertes est inspirée d'une proposition d'implémentation native du C++. La bibliothèque nécessite que l'utilisateur enregistre toutes les classes utilisées comme arguments virtuels (et leurs sous-classes), mais ne nécessite aucune modification du code existant. Les méthodes sont implémentées comme des fonctions C++ en ligne ordinaires ; elles peuvent être surchargées et transmises par pointeur. Il n'y a pas de limite au nombre d'arguments virtuels, et ils peuvent être mélangés arbitrairement avec des arguments non virtuels.
La bibliothèque utilise une combinaison de techniques (tables de répartition compressées, table de hachage d'entiers sans collision) pour implémenter les appels de méthode en temps constant, tout en réduisant l'utilisation de la mémoire. La répartition d'un appel à une méthode ouverte avec un seul argument virtuel ne prend que 15 à 30 % de temps de plus que l'appel d'une fonction membre virtuelle ordinaire, lorsqu'un compilateur d'optimisation moderne est utilisé.
L'exemple des astéroïdes peut être implémenté comme suit :
#include <yorel/yomm2/mots-clés.hpp> #include <mémoire> classe Chose { public : virtuel ~ Chose () {} }; classe Astéroïde : public Thing { }; classe Vaisseau spatial : public Thing { }; register_classes ( Chose , Vaisseau spatial , Astéroïde ); déclarer_méthode ( void , collideWith , ( virtual_ < Chose &> , virtual_ < Chose &> )); define_method ( void , collideWith , ( Thing & left , Thing & right )) { // gestion des collisions par défaut } define_method ( void , collideWith , ( Astéroïde & gauche , Astéroïde & droite )) { // gérer les collisions astéroïde-astéroïde } define_method ( void , collideWith , ( Astéroïde & gauche , Vaisseau spatial & droite )) { // gérer la collision astéroïde-vaisseau spatial } define_method ( void , collideWith , ( Vaisseau spatial & gauche , Astéroïde & droite )) { // gérer la collision vaisseau spatial-astéroïde } define_method ( void , collideWith , ( Spaceship & left , Spaceship & right )) { // gérer la collision vaisseau spatial-vaisseau spatial } int main () { yorel :: yomm2 :: méthodes_de_mise_à_jour (); std :: unique_ptr < Chose > a1 ( std :: make_unique < Astéroïde > ()), a2 ( std :: make_unique < Astéroïde > ()); std :: unique_ptr < Chose > s1 ( std :: make_unique < Vaisseau spatial > ()), s2 ( std :: make_unique < Vaisseau spatial > ()); // note : types partiellement effacés collideWith ( * a1 , * a2 ); // Collision astéroïde-astéroïde collideWith ( * a1 , * s1 ); // Collision astéroïde-vaisseau spatial collideWith ( * s1 , * a1 ); // Collision vaisseau spatial-astéroïde collideWith ( * s1 , * s2 ); // Collision vaisseau spatial-vaisseau spatial retourner 0 ; }
Stroustrup mentionne dans The Design and Evolution of C++ qu'il aimait le concept de multiméthodes et qu'il envisageait de l'implémenter en C++, mais affirme n'avoir pas réussi à trouver un exemple d'implémentation efficace (comparable aux fonctions virtuelles) et à résoudre certains problèmes possibles d'ambiguïté de type. Il déclare ensuite que même si la fonctionnalité serait toujours intéressante à avoir, elle peut être implémentée approximativement en utilisant un double dispatch ou une table de recherche basée sur le type comme indiqué dans l'exemple C/C++ ci-dessus, il s'agit donc d'une fonctionnalité de faible priorité pour les futures révisions du langage.
D
Depuis 2021 , comme de nombreux autres langages de programmation orientés objet, D ne prend en charge nativement que le dispatching unique. Cependant, il est possible d'émuler des méthodes multiples ouvertes en tant que fonction de bibliothèque en D. La bibliothèque openmethods en est un exemple.
// Déclaration Matrix plus ( virtual ! Matrix , virtual ! Matrix ); // Le remplacement pour deux objets DenseMatrix @method Matrix _plus ( DenseMatrix a , DenseMatrix b ) { const int nr = a . rows ; const int nc = a . cols ; assert ( a . nr == b . nr ); assert ( a . nc == b . nc ); auto result = new DenseMatrix ; result . nr = nr ; result . nc = nc ; result . elems . length = a . elems . length ; result . elems [] = a . elems [] + b . elems []; return result ; } // Le remplacement pour deux objets DiagonalMatrix @method Matrix _plus ( DiagonalMatrix a , DiagonalMatrix b ) { assert ( a . rows == b . rows ); double [] sum ; sum . length = a . elems . length ; sum [] = a . elems [] + b . elems []; return new DiagonalMatrix ( sum ); }
Java
Dans un langage avec un seul dispatch, comme Java , plusieurs dispatch peuvent être émulé avec plusieurs niveaux de dispatch unique :
interface Collideable { void collideWith ( final Collideable autre ); /* Ces méthodes auraient besoin de noms différents dans un langage sans surcharge de méthode. */ void collideWith ( final Asteroid asteroid ); void collideWith ( final Spaceship spaceship ); } classe Asteroid implémente Collideable { public void collideWith ( final Collideable other ) { // Appelez collideWith sur l'autre objet. other . collideWith ( this ); } public void collideWith ( final Asteroid asteroid ) { // Gère les collisions astéroïde-astéroïde. } public void collideWith ( final Spaceship spaceship ) { // Gère les collisions astéroïdes-vaisseaux spatiaux. } } classe Spaceship implémente Collideable { public void collideWith ( final Collideable other ) { // Appelez collideWith sur l'autre objet. other . collideWith ( this ); } public void collideWith ( final Asteroid asteroid ) { // Gère les collisions vaisseau spatial-astéroïde. } public void collideWith ( final Spaceship spaceship ) { // Gère les collisions vaisseau spatial-vaisseau spatial. } }
Des contrôles d'exécution instanceofà un ou aux deux niveaux peuvent également être utilisés.
Prise en charge des langages de programmation
Paradigme primaire
Prise en charge des multiméthodes générales
- C# 4.0
- Cécile
- Clojure
- Common Lisp (via le système d'objets Common Lisp )
- Dylan
- Emacs Lisp (via la méthode cl-def)
- Forteresse
- Groovy
- Lasso
- Nim , jusqu'à la version 0.19.x (à partir de la version 0.20.0, il est nécessaire de passer un indicateur de compilateur)
- Raku
- R
- Graine7
- TADS
- VB.Net via une liaison tardive, également via .Net DLR
- Wolfram Language via la correspondance de motifs symboliques
- Extension
Via des extensions
- Tout langage .NET (via la bibliothèque MultiMethods.NET)
- C (via la bibliothèque C Object System)
- C# (via la bibliothèque multimethod-sharp)
- C++ (via la bibliothèque yomm2, multimethods et omm)
- D (via la bibliothèque openmethods)
- Facteur (via le vocabulaire multiméthodes standard)
- Java (en utilisant l'extension MultiJava)
- JavaScript (via le package @arrows/multimethod)
- Perl (via le module Class::Multimethods)
- Python (via PEAK-Rules, RuleDispatch, gnosis.magic.multimethods, PyMultimethods, multipledispatch ou plum-dispatch)
- Raquette (via multimethod-lib)
- Ruby (via la bibliothèque The Multiple Dispatch Library et le package Multimethod et le package Vlx-Multimethods)
- Schéma (via par exemple TinyCLOS)
- TypeScript (via le paquet @arrows/multimethod)