web-dev-qa-db-fra.com

Explication simple des protocoles de fermeture

J'essaie de comprendre les protocoles de clôture et le problème qu'ils sont censés résoudre. Quelqu'un a-t-il une explication claire des tenants et aboutissants des protocoles de clôture?

123
Zubair

Le but des protocoles dans Clojure est de résoudre le problème d'expression de manière efficace.

Alors, quel est le problème d'expression? Il renvoie au problème de base de l'extensibilité: nos programmes manipulent les types de données à l'aide d'opérations. À mesure que nos programmes évoluent, nous devons les étendre avec de nouveaux types de données et de nouvelles opérations. Et en particulier, nous voulons être en mesure d'ajouter de nouvelles opérations qui fonctionnent avec les types de données existants, et nous voulons ajouter de nouveaux types de données qui fonctionnent avec les opérations existantes. Et nous voulons que cela soit vrai extension , c'est-à-dire que nous ne le faisons pas nous voulons modifier le programme existant , nous voulons respecter les abstractions existantes, nous voulons que nos extensions soient des modules séparés, dans des espaces de noms séparés, compilés séparément, séparément déployé, type vérifié séparément. Nous voulons qu'ils soient de type sûr. [Remarque: tous ces éléments n'ont pas de sens dans toutes les langues. Mais, par exemple, l'objectif de les faire sécuriser est logique même dans une langue comme Clojure. Ce n'est pas parce que nous ne pouvons pas statiquement vérifier la sécurité de type que nous voulons que notre code se casse au hasard, non?]

Le problème d'expression est, comment pouvez-vous réellement fournir une telle extensibilité dans une langue?

Il s'avère que pour les implémentations naïves typiques de programmation procédurale et/ou fonctionnelle, il est très facile d'ajouter de nouvelles opérations (procédures, fonctions), mais très difficile d'ajouter de nouveaux types de données, car en gros, les opérations fonctionnent avec les types de données en utilisant certains sorte de discrimination de casse (switch, case, filtrage) et vous devez leur ajouter de nouveaux cas, c'est-à-dire modifier le code existant:

func print(node):
  case node of:
    AddOperator => print(node.left) + '+' + print(node.right)
    NotOperator => '!' + print(node)

func eval(node):
  case node of:
    AddOperator => eval(node.left) + eval(node.right)
    NotOperator => !eval(node)

Maintenant, si vous voulez ajouter une nouvelle opération, disons, la vérification de type, c'est facile, mais si vous voulez ajouter un nouveau type de nœud, vous devez modifier toutes les expressions de correspondance de modèle existantes dans toutes les opérations.

Et pour un OO naïf typique, vous avez exactement le problème opposé: il est facile d'ajouter de nouveaux types de données qui fonctionnent avec les opérations existantes (soit en les héritant soit en les remplaçant), mais il est difficile d'ajouter de nouvelles opérations, car cela signifie essentiellement modifier classes/objets existants.

class AddOperator(left: Node, right: Node) < Node:
  meth print:
    left.print + '+' + right.print

  meth eval
    left.eval + right.eval

class NotOperator(expr: Node) < Node:
  meth print:
    '!' + expr.print

  meth eval
    !expr.eval

Ici, l'ajout d'un nouveau type de nœud est facile, car vous héritez, remplacez ou implémentez toutes les opérations requises, mais l'ajout d'une nouvelle opération est difficile, car vous devez l'ajouter à toutes les classes feuilles ou à une classe de base, modifiant ainsi l'existant code.

Plusieurs langages ont plusieurs constructions pour résoudre le problème d'expression: Haskell a des classes de types, Scala a des arguments implicites, Racket a des unités, Go a des interfaces, CLOS et Clojure ont des méthodes multimédias. Il y a aussi des "solutions" qui essayez de le résoudre, mais échouez d'une manière ou d'une autre: Interfaces et méthodes d'extension en C # et Java, Monkeypatching en Ruby, Python, ECMAScript.

Notez que Clojure possède déjà un mécanisme pour résoudre le problème d'expression: multiméthodes. Le problème que OO a avec l'EP est qu'ils regroupent les opérations et les types. Avec Multimethods, ils sont séparés. Le problème que FP a est qu'ils regroupent l'opération et la discrimination de cas ensemble. Encore une fois, avec Multimethods ils sont séparés.

Comparons donc les protocoles aux multiméthodes, car les deux font la même chose. Ou, pour le dire autrement: pourquoi les protocoles si nous avons déjà déjà multiméthodes?

La principale chose que les protocoles offrent par rapport aux méthodes multimédias est le regroupement: vous pouvez regrouper plusieurs fonctions et dire "ces 3 fonctions ensemble forment le protocole Foo" . Vous ne pouvez pas faire ça avec les Multimethods, elles sont toujours autonomes. Par exemple, vous pouvez déclarer qu'un Stack protocole se compose de à la fois un Push et un pop fonction ensemble .

Alors, pourquoi ne pas simplement ajouter la possibilité de regrouper les multiméthodes? Il y a une raison purement pragmatique, et c'est pourquoi j'ai utilisé le mot "efficace" dans ma phrase introductive: performance.

Clojure est une langue hébergée. C'est à dire. il est spécifiquement conçu pour être exécuté sur la plate-forme d'une autre langue . Et il s'avère que pratiquement toutes les plates-formes sur lesquelles vous aimeriez que Clojure s'exécute (JVM, CLI, ECMAScript, Objective-C) ont un support haute performance spécialisé pour la répartition uniquement sur le type du premier argument. Clojure Multimethods OTOH répartit sur les propriétés arbitraires de tous les arguments .

Ainsi, les protocoles vous limitent à envoyer uniquement sur le premier argument et uniquement sur son type (ou comme cas particulier sur nil).

Ce n'est pas une limitation de l'idée des protocoles en soi, c'est un choix pragmatique pour avoir accès aux optimisations de performances de la plateforme sous-jacente. En particulier, cela signifie que les protocoles ont un mappage trivial avec les interfaces JVM/CLI, ce qui les rend très rapides. Assez rapide, en fait, pour pouvoir réécrire les parties de Clojure qui sont actuellement écrites en Java ou C # dans Clojure lui-même.

Clojure possède déjà des protocoles depuis la version 1.0: Seq est un protocole, par exemple. Mais jusqu'à la version 1.2, vous ne pouviez pas écrire de protocoles dans Clojure, vous deviez les écrire dans la langue de l'hôte.

267
Jörg W Mittag

Je trouve très utile de penser que les protocoles sont conceptuellement similaires à une "interface" dans des langages orientés objet tels que Java. Un protocole définit un ensemble abstrait de fonctions qui peuvent être implémentées de manière concrète pour un objet donné.

Un exemple:

(defprotocol my-protocol 
  (foo [x]))

Définit un protocole avec une fonction appelée "foo" qui agit sur un paramètre "x".

Vous pouvez ensuite créer des structures de données qui implémentent le protocole, par exemple.

(defrecord constant-foo [value]  
  my-protocol
    (foo [x] value))

(def a (constant-foo. 7))

(foo a)
=> 7

Notez qu'ici, l'objet implémentant le protocole est passé comme premier paramètre x - un peu comme le paramètre implicite "this" dans les langages orientés objet.

L'une des caractéristiques très puissantes et utiles des protocoles est que vous pouvez les étendre aux objets même si l'objet n'a pas été initialement conçu pour prendre en charge le protocole. par exemple. vous pouvez étendre le protocole ci-dessus à la classe Java.lang.String si vous le souhaitez:

(extend-protocol my-protocol
  Java.lang.String
    (foo [x] (.length x)))

(foo "Hello")
=> 5
64
mikera