Protocoles
Motivation
Clojure est écrit en termes d'abstractions. Il existe des abstractions pour les séquences, les collections, la possibilité d'appeler des fonctions,... De plus, Clojure fournit de nombreuses implémentations de ces abstractions. Les abstractions sont spécifiées par des interfaces hôtes, et les implémentations par des classes hôtes. Bien que cela ait suffi pour initialiser le langage, Clojure manquait alors de mécanismes d'abstraction et d'implémentation de bas niveau similaires. Les protocoles et les types de données ajoutent des mécanismes puissants et flexibles pour l'abstraction et la définition des structures de données, sans compromis sur les fonctionnalités de la plateforme hôte.
Les protocoles présentent plusieurs avantages :
- Offrir une construction de polymorphisme dynamique et performante comme alternative aux interfaces
- Prendre en charge les avantages des interfaces
- Spécification uniquement, sans implémentation
- Un seul type peut implémenter plusieurs protocoles
- Tout en évitant certains inconvénients :
- Le choix des interfaces implémentées relève de la conception du type et ne peut être étendu ultérieurement (bien que l'injection d'interfaces puisse éventuellement résoudre ce problème)
- L'implémentation d'une interface crée une relation et une hiérarchie de types «isa»/«instanceof»
- Éviter le «problème d'expression» en permettant l'extension indépendante de l'ensemble des types, des protocoles et des implémentations de protocoles sur les types, par différentes parties
- et ce, sans utiliser de wrappers ni adapters
- Prendre en charge 90% des cas de multiméthodes (appel unique sur le type) tout en fournissant une abstraction et une organisation de plus haut niveau
Les protocoles ont été introduits dans Clojure 1.2.
Notions de base
Un protocole est un ensemble nommé de méthodes nommées et de leurs signatures, défini à l'aide de la fonction `defprotocol` :
- (defprotocol AProtocol
- "Une chaîne de documentation pour une abstraction de protocole"
- (bar [a b] "bar docs")
- (baz [a] [a b] [a b c] "baz docs"))
- Aucune implémentation n'est fournie.
- La documentation peut être spécifiée pour le protocole et les fonctions.
- Le code ci-dessus génère un ensemble de fonctions polymorphes et un objet protocole.
- Toutes les fonctions sont qualifiées par l'espace de noms contenant leur définition.
- Les fonctions résultantes effectuent un dispatch sur le type de leur premier argument et doivent donc avoir au moins un argument.
- La définition de `defprotocol` est dynamique et ne nécessite pas de compilation AOT.
- Remarque : les annotations de type primitif ne sont pas prises en charge pour les fonctions de protocole.
La fonction `defprotocol` génère automatiquement une interface correspondante, portant le même nom que le protocole. Par exemple, pour un protocole `my.ns/Protocol`, une interface `my.ns.Protocol` est créée. Cette interface possède des méthodes correspondant aux fonctions du protocole, et le protocole fonctionne automatiquement avec les instances de cette interface.
Notez qu'il est inutile d'utiliser cette interface avec `deftype`, `defrecord` ou `reify`, car ces fonctions gèrent directement les protocoles.
- (defprotocol P
- (foo [x])
- (bar-me [x] [x y]))
-
- (deftype Foo [a b c]
- P
- (foo [x] a)
- (bar-me [x] b)
- (bar-me [x y] (+ c y)))
-
- (bar-me (Foo. 1 2 3) 42)
- = > 45
-
- (foo
- (let [x 42]
- (reify P
- (foo [this] 17)
- (bar-me [this] x)
- (bar-me [this y] x))))
-
- > 17
Un client Java souhaitant participer au protocole peut le faire de manière optimale en implémentant l'interface générée par le protocole.
Les implémentations externes du protocole (nécessaires lorsqu'une classe ou un type externe doit participer au protocole) peuvent être fournies à l'aide du mot-clef `extend` :
- (extend AType
- AProtocol
- {:foo an-existing-fn
- :bar (fn [a b] ...)
- :baz (fn ([a]...) ([a b] ...)...)}
- BProtocol
- {...}
- ...)
La méthode `extend` prend un type/une classe (ou une interface, voir ci-dessous) et une ou plusieurs paires protocole + fonction (évaluée) :
- Elle étend le polymorphisme des méthodes du protocole pour appeler les fonctions fournies lorsqu'un `AType` est passé comme premier argument.
- Les fonctions associent les noms de méthodes (définis par des mots-clefs) à des fonctions ordinaires.
- Ceci facilite la réutilisation des fonctions et des fonctions existantes, pour la réutilisation de code et les mixins sans dérivation ni composition.
- Vous pouvez implémenter un protocole sur une interface.
- Ceci facilite principalement l'interopérabilité avec l'hôte (par exemple, Java).
- Cependant, cela ouvre la porte à l'héritage multiple accidentel d'implémentations.
- Puisqu'une classe peut hériter de plusieurs interfaces, implémentant toutes le protocole.
- Si une interface dérive de l'autre, c'est l'interface la plus dérivée qui est utilisée ; sinon, l'interface utilisée n'est pas spécifiée.
- La fonction d'implémentation peut supposer que le premier argument est une instance de `AType`.
- Vous pouvez implémenter un protocole sur `nil`.
- Pour définir une implémentation par défaut du protocole (pour une valeur autre que `nil`), utilisez simplement `Object`.
Les protocoles sont entièrement concrétisés et prennent en charge les fonctionnalités de réflexion via `extends?`, `extenders` et `satisfies?`.
- Notez les macros pratiques `extend-type` et `extend-protocol`.
- Si vous fournissez des définitions externes directement dans le code, ces macros seront plus pratiques que l'utilisation directe de `extend`.
- (extend-type MyType
- Countable
- (cnt [c] ...)
- Foo
- (bar [x y] ...)
- (baz ([x] ...) ([x y zs] ...)))
-
- ;se développe en :
-
- (extend MyType
- Countable
- {:cnt (fn [c] ...)}
- Foo
- {:baz (fn ([x] ...) ([x y zs] ...))
- :bar (fn [x y] ...)})
Recommandations pour l'extension
Les protocoles sont un système ouvert, extensible à tout type. Pour minimiser les conflits, veuillez tenir compte des recommandations suivantes :
- Si vous n'êtes pas propriétaire du protocole ou du type cible, vous ne devez l'étendre que dans le code de l'application (et non dans une bibliothèque publique), et vous pouvez être conscient que son fonctionnement peut être perturbé par l'un ou l'autre des propriétaires.
- Si vous êtes propriétaire du protocole, vous pouvez fournir des versions de base pour les cibles courantes dans le cadre du paquet, sous réserve du caractère contraignant de cette démarche.
- Si vous distribuez une bibliothèque de cibles potentielles, vous pouvez fournir des implémentations des protocoles courants pour celles-ci, sous réserve du fait que vous en imposez les modalités. Vous devez être particulièrement vigilant lorsque vous étendez des protocoles inclus dans Clojure.
- Si vous êtes développeur de bibliothèque, vous ne devez pas étendre un protocole si vous n'en êtes propriétaire ni de celui-ci ni de la cible.
Voir également cette discussion sur la liste de diffusion.
Extension via métadonnées
Depuis Clojure 1.10, les protocoles peuvent, en option, être étendus via des métadonnées par valeur :
- (defprotocol Component
- :extend-via-metadata true
- (start [component]))
Lorsque l'option `:extend-via-metadata` est activée, les valeurs peuvent étendre les protocoles en ajoutant des métadonnées où les clefs sont des symboles de fonction de protocole pleinement qualifiés et les valeurs sont des implémentations de fonctions. Les implémentations de protocole sont d'abord vérifiées pour les définitions directes (`defrecord`, `deftype`, `reify`), puis les définitions de métadonnées, et enfin les extensions externes (`extend`, `extend-type`, `extend-protocol`).
- (def component (with-meta {:name "db"} {`start (constantly "started")}))
- (start component)
- ;;=> "started"