Einfache Erklärung der Clojure-Protokolle


Antworten:


284

Der Zweck von Protokollen in Clojure besteht darin, das Ausdrucksproblem auf effiziente Weise zu lösen.

Also, was ist das Ausdrucksproblem? Es bezieht sich auf das Grundproblem der Erweiterbarkeit: Unsere Programme bearbeiten Datentypen mithilfe von Operationen. Während sich unsere Programme weiterentwickeln, müssen wir sie um neue Datentypen und neue Operationen erweitern. Insbesondere möchten wir in der Lage sein, neue Operationen hinzuzufügen, die mit den vorhandenen Datentypen funktionieren, und wir möchten neue Datentypen hinzufügen, die mit den vorhandenen Operationen funktionieren. Und wir möchten, dass dies eine echte Erweiterung ist , dh wir möchten die vorhandene nicht ändernProgramm, wir möchten die vorhandenen Abstraktionen respektieren, wir möchten, dass unsere Erweiterungen separate Module sind, in separaten Namespaces, separat kompiliert, separat bereitgestellt, separat typgeprüft. Wir möchten, dass sie typsicher sind. [Hinweis: Nicht alle davon sind in allen Sprachen sinnvoll. Aber zum Beispiel macht das Ziel, sie typsicher zu machen, auch in einer Sprache wie Clojure Sinn. Nur weil wir die Typensicherheit nicht statisch überprüfen können, heißt das nicht, dass unser Code zufällig unterbrochen werden soll, oder?]

Das Ausdrucksproblem ist, wie Sie tatsächlich eine solche Erweiterbarkeit in einer Sprache bereitstellen?

Es stellt sich heraus, dass es für typische naive Implementierungen der prozeduralen und / oder funktionalen Programmierung sehr einfach ist, neue Operationen (Prozeduren, Funktionen) hinzuzufügen, aber sehr schwierig, neue Datentypen hinzuzufügen, da die Operationen im Grunde mit den Datentypen arbeiten, die einige verwenden Art Fall Diskriminierung ( switch, case, pattern - Matching) und Sie müssen neue Fälle zu ihnen, also ändern vorhandenen Code hinzuzufügen:

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)

Wenn Sie nun eine neue Operation hinzufügen möchten, z. B. die Typprüfung, ist dies einfach. Wenn Sie jedoch einen neuen Knotentyp hinzufügen möchten, müssen Sie alle vorhandenen Mustervergleichsausdrücke in allen Operationen ändern.

Und für ein typisches naives OO haben Sie genau das gegenteilige Problem: Es ist einfach, neue Datentypen hinzuzufügen, die mit den vorhandenen Operationen funktionieren (entweder durch Erben oder Überschreiben), aber es ist schwierig, neue Operationen hinzuzufügen, da dies im Grunde genommen eine Änderung bedeutet vorhandene Klassen / Objekte.

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

Hier ist das Hinzufügen eines neuen Knotentyps einfach, da Sie alle erforderlichen Operationen entweder erben, überschreiben oder implementieren. Das Hinzufügen einer neuen Operation ist jedoch schwierig, da Sie sie entweder allen Blattklassen oder einer Basisklasse hinzufügen müssen, um vorhandene zu ändern Code.

Mehrere Sprachen haben verschiedene Konstrukte zur Lösung des Ausdrucksproblems: Haskell hat Typklassen, Scala hat implizite Argumente, Racket hat Einheiten, Go hat Schnittstellen, CLOS und Clojure haben Multimethoden. Es gibt auch "Lösungen", die versuchen , das Problem zu lösen, aber auf die eine oder andere Weise fehlschlagen: Schnittstellen und Erweiterungsmethoden in C # und Java, Monkeypatching in Ruby, Python, ECMAScript.

Beachten Sie, dass Clojure bereits über einen Mechanismus zur Lösung des Ausdrucksproblems verfügt: Multimethoden. Das Problem, das OO mit dem EP hat, ist, dass sie Operationen und Typen zusammen bündeln. Bei Multimethoden sind sie getrennt. Das Problem, das FP hat, ist, dass sie die Operation und die Fallunterscheidung zusammen bündeln. Auch bei Multimethoden sind sie getrennt.

Vergleichen wir also Protokolle mit Multimethoden, da beide dasselbe tun. Oder, um es anders auszudrücken: Warum Protokolle , wenn wir bereits haben Multimethoden ?

Die Hauptsache Protokolle bieten über Multimethoden ist Gruppierung: Sie können Gruppe mehrere Funktionen zusammen und sagen : „Diese drei Funktionen zusammen Protokoll bilden Foo“. Mit Multimethoden ist das nicht möglich, sie stehen immer für sich. Zum Beispiel könnte man erklären , dass ein StackProtokoll besteht aus den beiden ein pushund popFunktion zusammen .

Warum also nicht einfach die Möglichkeit hinzufügen, Multimethoden zu gruppieren? Es gibt einen rein pragmatischen Grund, und deshalb habe ich in meinem Einleitungssatz das Wort "effizient" verwendet: Leistung.

Clojure ist eine gehostete Sprache. Das heißt, es wurde speziell entwickelt, um auf der Plattform einer anderen Sprache ausgeführt zu werden. Und es stellt sich heraus, dass so ziemlich jede Plattform, auf der Clojure ausgeführt werden soll (JVM, CLI, ECMAScript, Objective-C), über eine spezialisierte Hochleistungsunterstützung verfügt, die ausschließlich auf den Typ des ersten Arguments ausgerichtet ist. Clojure Multimethoden OTOH Versand auf beliebige Eigenschaften von allen Argumenten .

Protokolle beschränken Sie also darauf, nur beim ersten Argument und nur bei seinem Typ (oder als Sonderfall bei nil) zu versenden .

Dies ist keine Einschränkung der Idee von Protokollen an sich, sondern eine pragmatische Entscheidung, um Zugriff auf die Leistungsoptimierungen der zugrunde liegenden Plattform zu erhalten. Dies bedeutet insbesondere, dass Protokolle eine einfache Zuordnung zu JVM / CLI-Schnittstellen aufweisen, wodurch sie sehr schnell sind. Tatsächlich schnell genug, um die Teile von Clojure, die derzeit in Java oder C # in Clojure selbst geschrieben sind, neu schreiben zu können.

Clojure hat tatsächlich bereits seit Version 1.0 SeqProtokolle : ist beispielsweise ein Protokoll. Bis 1.2 konnten Sie jedoch keine Protokolle in Clojure schreiben, sondern mussten sie in der Hostsprache schreiben.


Vielen Dank für eine so gründliche Antwort, aber können Sie Ihren Standpunkt zu Ruby klarstellen? Ich nehme an, dass die Fähigkeit, Methoden einer Klasse (z. B. String, Fixnum) in Ruby (neu) zu definieren, eine Analogie zu Clojures Defprotokoll ist.
Defhlt

3
Ein ausgezeichneter Artikel über das Ausdrucksproblem und die Protokolle von clojure
ibm.com/developerworks/library/j-clojure-protocols

Es tut uns leid, einen Kommentar zu einer so alten Antwort zu schreiben, aber können Sie näher erläutern, warum Erweiterungen und Schnittstellen (C # / Java) keine gute Lösung für das Ausdrucksproblem sind?
Onorio Catenacci

Java hat keine Erweiterungen in dem Sinne, dass der Begriff hier verwendet wird.
user100464

Ruby hat Verfeinerungen, die das Patchen von Affen überflüssig machen.
Marcin Bilski

64

Ich finde es am hilfreichsten, sich Protokolle als konzeptionell ähnlich wie eine "Schnittstelle" in objektorientierten Sprachen wie Java vorzustellen. Ein Protokoll definiert einen abstrakten Satz von Funktionen, die für ein bestimmtes Objekt konkret implementiert werden können.

Ein Beispiel:

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

Definiert ein Protokoll mit einer Funktion namens "foo", das auf einen Parameter "x" wirkt.

Sie können dann Datenstrukturen erstellen, die das Protokoll implementieren, z

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

(def a (constant-foo. 7))

(foo a)
=> 7

Beachten Sie, dass hier das Objekt, das das Protokoll implementiert, als erster Parameter übergeben wird x- ähnlich wie der implizite "this" -Parameter in objektorientierten Sprachen.

Eine der sehr leistungsfähigen und nützlichen Funktionen von Protokollen besteht darin, dass Sie sie auf Objekte erweitern können, auch wenn das Objekt ursprünglich nicht zur Unterstützung des Protokolls entwickelt wurde . Sie können beispielsweise das obige Protokoll auf die Klasse java.lang.String erweitern, wenn Sie möchten:

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

(foo "Hello")
=> 5

1
> Wie der implizite "this" -Parameter in objektorientierter Sprache ist mir aufgefallen, dass die an die Protokollfunktionen übergebene Variable häufig auch thisim Clojure-Code aufgerufen wird.
Kris
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.