Multiméthodes et hiérarchies
Clojure s'affranchit de l'approche orientée objet traditionnelle consistant à créer un nouveau type de données pour chaque situation, préférant construire une vaste bibliothèque de fonctions sur un petit ensemble de types. Cependant, Clojure reconnaît pleinement l'importance du polymorphisme d'exécution pour une architecture système flexible et extensible. Clojure prend en charge un polymorphisme d'exécution sophistiqué grâce à un système de multiméthodes permettant d'effectuer des appels sur les types, les valeurs, les attributs et les métadonnées d'un ou plusieurs arguments, ainsi que sur les relations entre eux.
Une multiméthode Clojure est la combinaison d'une fonction de répartition et d'une ou plusieurs méthodes. Lors de la définition d'une multiméthode, avec `defmulti`, une fonction de répartition doit être fournie. Cette fonction sera appliquée aux arguments de la multiméthode afin de produire une valeur de répartition. La multiméthode tentera ensuite de trouver la méthode associée à cette valeur ou une valeur dont elle est dérivée. Si une telle méthode a été définie (via `defmethod`), elle sera appelée avec les arguments, et cette méthode constituera la valeur de l'appel de la multiméthode. Si aucune méthode n'est associée à la valeur de répartition, la multiméthode recherchera une méthode associée à la valeur de répartition par défaut (qui est `:default`) et l'utilisera si elle existe. Sinon, l'appel générera une erreur.
Le système de multiméthodes expose l'API suivante : `defmulti` crée de nouvelles multiméthodes, `defmethod` crée et installe une nouvelle méthode de multiméthode associée à une valeur de répartition, `remove-method` supprime la méthode associée à une valeur de répartition et `prefer-method` établit un ordre entre les méthodes lorsqu'elles seraient autrement ambiguës.
La dérivation est déterminée par une combinaison d'héritage Java (pour les valeurs de classe) ou par le système hiérarchique ad hoc de Clojure. Ce système hiérarchique prend en charge les relations de dérivation entre les noms (symboles ou mots-clés) et les relations entre les classes et les noms. La fonction `derive` crée ces relations et la fonction `isa?` vérifie leur existence. Notez que `isa?` est différent de `instance?`.
Vous pouvez définir des relations hiérarchiques avec `(derive child parent)`. L'enfant et le parent peuvent être des symboles ou des mots-clefs et doivent être qualifiés par un espace de noms :
Notez la syntaxe `::reader`, tandis que `::keywords` résout les espaces de noms.
- ::rect
- -> :user/rect
derive est le créateur de relations fondamental (relationship-maker) :
- (derive ::rect ::shape)
- (derive ::square ::rect)
parents / ancestors / descendants et isa? permet d'interroger la hiérarchie :
- (parents ::rect)
- -> #{:user/shape}
-
- (ancestors ::square)
- -> #{:user/rect :user/shape}
-
- (descendants ::shape)
- -> #{:user/rect :user/square}
(= x y) implique (isa? x y) :
- (isa? 42 42)
- -> true
isa? utilise le système hiérarchique :
- (isa? ::square ::shape)
- -> true
Vous pouvez également utiliser une classe comme enfant (mais pas comme parent ; la seule façon de définir un élément comme enfant d'une classe est par le biais de l'héritage en Java).
Cela vous permet de superposer de nouvelles taxonomies à la hiérarchie de classes Java existante :
- (derive java.util.Map ::collection)
- (derive java.util.Collection ::collection)
-
- (isa? java.util.HashMap ::collection)
- -> true
isa? teste également les relations entre classes :
- (isa? String Object)
- -> true
de même que les parents / ancestors (mais pas les descendants, puisque les descendants de classe constituent un ensemble ouvert).
- (ancestors java.util.ArrayList)
- -> #{java.lang.Cloneable java.lang.Object java.util.List
- java.util.Collection java.io.Serializable
- java.util.AbstractCollection
- java.util.RandomAccess java.util.AbstractList}
La fonction `isa?` fonctionne avec les vecteurs en appelant `isa?` sur leurs éléments correspondants :
- (isa? [::square ::rect] [::shape ::shape])
- -> true
dispatch basé sur `isa?`
Les méthodes multiples utilisent `isa?` plutôt que `=` pour tester la correspondance des valeurs de dispatch. Notez que le premier test de `isa?` renvoie `=`, donc les correspondances exactes fonctionnent :
- (defmulti foo class)
- (defmethod foo ::collection [c] :a-collection)
- (defmethod foo String [s] :a-string)
-
- (foo [])
- :a-collection
-
- (foo (java.util.HashMap.))
- :a-collection
-
- (foo "bar")
- :a-string
prefer-method permet de lever l'ambiguïté en cas de correspondances multiples où aucune valeur ne prédomine sur l'autre. Il suffit de déclarer, pour chaque méthode multiple, qu'une valeur de répartition est préférée à une autre :
- (derive ::rect ::shape)
-
- (defmulti bar (fn [x y] [x y]))
- (defmethod bar [::rect ::shape] [x y] :rect-shape)
- (defmethod bar [::shape ::rect] [x y] :shape-rect)
-
- (bar ::rect ::rect)
- -> Execution error (IllegalArgumentException) at user/eval152 (REPL:1).
- Multiple methods in multimethod 'bar' match dispatch value:
- [:user/rect :user/rect] -> [:user/shape :user/rect]
- and [:user/rect :user/shape], and neither is preferred
-
- (prefer-method bar [::rect ::shape] [::shape ::rect])
- (bar ::rect ::rect)
- -> :rect-shape
Tous les exemples ci-dessus utilisent la hiérarchie globale du système multiméthode, mais il est également possible de créer des hiérarchies entièrement indépendantes avec `make-hierarchy`. De plus, toutes les fonctions mentionnées peuvent prendre une hiérarchie optionnelle comme premier argument.
Ce système simple est extrêmement puissant. Pour comprendre la relation entre les multiméthodes Clojure et la fonction de répartition unique traditionnelle de Java, on peut considérer que la répartition unique est comparable à une multiméthode Clojure dont la fonction de répartition appelle `getClass` sur le premier argument et dont les méthodes sont associées à ces classes. Les multiméthodes Clojure ne sont pas figées à une classe ou un type ; elles peuvent être basées sur n'importe quel attribut des arguments, sur plusieurs arguments, valider les arguments et gérer les erreurs,...
Remarque : Dans cet exemple, le mot-clef `:Shape` est utilisé comme fonction de répartition, car les mots-clefs sont des fonctions de cartographie, comme expliqué dans la section Structures de données.
- (defmulti area :Shape)
- (defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
- (defn circle [radius] {:Shape :Circle :radius radius})
- (defmethod area :Rect [r]
- (* (:wd r) (:ht r)))
- (defmethod area :Circle [c]
- (* (. Math PI) (* (:radius c) (:radius c))))
- (defmethod area :default [x] :oops)
- (def r (rect 4 13))
- (def c (circle 12))
- (area r)
- -> 52
- (area c)
- -> 452.3893421169302
- (area {})
- -> :oops