APIs und funktionale Programmierung


15

Aufgrund meiner (zugegebenermaßen eingeschränkten) Erfahrung mit funktionalen Programmiersprachen wie Clojure scheint die Kapselung von Daten eine weniger wichtige Rolle zu spielen. Normalerweise sind verschiedene native Typen wie Karten oder Mengen die bevorzugte Währung für die Darstellung von Daten gegenüber Objekten. Darüber hinaus sind diese Daten im Allgemeinen unveränderlich.

Hier zum Beispiel eines der bekannteren Zitate von Rich Hickey von Clojure in einem Interview zu diesem Thema :

Fogus: Dieser Idee folgend - einige Leute sind überrascht über die Tatsache, dass Clojure keine datenversteckenden Kapselungen für seine Typen vornimmt. Warum haben Sie sich entschieden, auf das Verbergen von Daten zu verzichten?

Hickey: Lassen Sie uns klarstellen, dass Clojure die Programmierung von Abstraktionen stark betont. Irgendwann muss jemand Zugriff auf die Daten haben. Und wenn Sie eine Vorstellung von „privat“ haben, brauchen Sie entsprechende Vorstellungen von Privilegien und Vertrauen. Und das erhöht die Komplexität und den Wert des Systems um eine ganze Menge, schafft Starrheit in einem System und zwingt die Dinge oft dazu, an Orten zu leben, an denen sie nicht sein sollten. Dies kommt zu den anderen Verlusten hinzu, die auftreten, wenn einfache Informationen in Klassen eingeteilt werden. In dem Maße, in dem die Daten unveränderlich sind, kann die Bereitstellung des Zugriffs nur wenig schaden, außer dass jemand von etwas abhängen könnte, das sich ändern könnte. Okay, die Leute machen das die ganze Zeit im wirklichen Leben und wenn sich die Dinge ändern, passen sie sich an. Und wenn sie rational sind, Sie wissen, wann sie eine Entscheidung treffen, die auf etwas basiert, das sich ändern kann und das sie möglicherweise in Zukunft anpassen müssen. Es ist also eine Risikomanagement-Entscheidung, die Programmierer meiner Meinung nach frei treffen sollten. Wenn die Leute nicht die Sensibilität haben, auf Abstraktionen programmieren zu wollen und die Details der Implementierung nicht zu kennen, werden sie niemals gute Programmierer sein.

Aus der OO-Welt stammend, scheint dies einige der verankerten Prinzipien, die ich im Laufe der Jahre gelernt habe, zu verkomplizieren. Dazu gehören Information Hiding, das Demeter-Gesetz und das Prinzip des einheitlichen Zugriffs, um nur einige zu nennen. Der rote Faden ist, dass die Kapselung es uns ermöglicht, eine API zu definieren, damit andere wissen, was sie berühren sollen und was nicht. Im Wesentlichen sollte ein Vertrag erstellt werden, der es dem Betreuer eines Codes ermöglicht, Änderungen und Umgestaltungen frei vorzunehmen, ohne sich Gedanken darüber zu machen, wie Fehler in den Code des Verbrauchers eingefügt werden könnten (Open / Closed-Prinzip). Es bietet auch eine saubere, kuratierte Oberfläche, über die andere Programmierer wissen, welche Tools sie verwenden können, um auf diese Daten zuzugreifen oder darauf aufzubauen.

Wenn auf die Daten direkt zugegriffen werden darf, ist dieser API-Vertrag ungültig und alle diese Vorteile der Kapselung scheinen verschwunden zu sein. Darüber hinaus scheinen streng unveränderliche Daten die Weitergabe domänenspezifischer Strukturen (Objekte, Strukturen, Datensätze) im Sinne der Darstellung eines Zustands und der Menge von Aktionen, die für diesen Zustand ausgeführt werden können, weniger nützlich zu machen.

Wie gehen funktionale Codebasen mit diesen Problemen um, die auftreten, wenn die Größe einer Codebasis enorm zunimmt, sodass APIs definiert werden müssen und viele Entwickler an der Arbeit mit bestimmten Teilen des Systems beteiligt sind? Gibt es Beispiele für diese Situation, die veranschaulichen, wie dies in solchen Codebasen gehandhabt wird?


2
Sie können eine formale Schnittstelle ohne Objektbegriff definieren. Erstellen Sie einfach die Funktion der Schnittstelle, die sie dokumentiert. Stellen Sie keine Dokumentation für Implementierungsdetails bereit. Sie haben gerade eine Schnittstelle erstellt.
Scara95

@ Scara95 Bedeutet das nicht, dass ich arbeiten muss, um den Code für eine Schnittstelle zu implementieren und genug Dokumentation darüber zu schreiben, um den Verbraucher zu warnen, was zu tun ist und was nicht? Was ist, wenn sich der Code ändert und die Dokumentation veraltet ist? Aus diesem Grund bevorzuge ich im Allgemeinen selbstdokumentierenden Code.
Jameslk

Sie müssen die Schnittstelle trotzdem dokumentieren.
Scara95

3
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.Nicht wirklich. Das einzige, was sich ändert, ist, dass die Änderungen auf einem neuen Objekt enden. Dies ist ein großer Gewinn, wenn es darum geht, über den Code nachzudenken. Wenn Sie veränderbare Objekte weitergeben, müssen Sie nachverfolgen, wer sie möglicherweise verändert. Dieses Problem hängt von der Größe des Codes ab.
Doval

Antworten:


10

Zuallererst gehe ich zu Sebastians Kommentaren über das Funktionale und das Dynamische Tippen über. Im Allgemeinen ist Clojure eine Variante der funktionalen Sprache und Community, und Sie sollten nicht zu viel darauf verallgemeinern. Ich werde einige Bemerkungen aus einer ML / Haskell-Perspektive machen.

Wie Basile erwähnt, existiert das Konzept der Zugangskontrolle in ML / Haskell und wird häufig verwendet. Das "Factoring" unterscheidet sich ein wenig von herkömmlichen OOP-Sprachen. In OOP spielt das Konzept einer Klasse gleichzeitig die Rolle von Typ und Modul , während funktionale (und traditionelle prozedurale) Sprachen diese orthogonal behandeln.

Ein weiterer Punkt ist, dass ML / Haskell sehr viele Generika mit Typ-Erasure-Funktion enthalten und dass dies verwendet werden kann, um eine andere Art des "Versteckens von Informationen" als die OOP-Kapselung bereitzustellen. Wenn eine Komponente nur den Typ eines Datenelements als Typparameter kennt, können dieser Komponente Werte dieses Typs sicher übergeben werden, und es wird dennoch verhindert, dass sie viel damit anfangen, da sie ihren konkreten Typ nicht kennt und nicht kennen kann ( instanceofIn diesen Sprachen gibt es kein universelles Casting oder Laufzeit-Casting.) Dieser Blog-Eintrag ist eines meiner bevorzugten Einführungsbeispiele für diese Techniken.

Weiter: In der FP-Welt ist es sehr verbreitet, transparente Datenstrukturen als Schnittstellen zu undurchsichtigen / gekapselten Komponenten zu verwenden. Beispielsweise sind Interpreter-Muster in FP sehr verbreitet, bei denen Datenstrukturen als Syntaxbäume verwendet werden, die Logik beschreiben, und dem Code zugeführt werden, der sie "ausführt". Der Zustand existiert dann, richtig gesagt, nur vorübergehend, wenn der Interpreter ausgeführt wird, der die Datenstrukturen verbraucht. Auch die Implementierung des Interpreters kann sich ändern, solange er mit den Clients in Bezug auf dieselben Datentypen kommuniziert.

Letzte und längste: Das Verbergen von Informationen ist eine Technik , kein Ende. Lassen Sie uns ein wenig darüber nachdenken, was es bietet. Die Kapselung ist eine Technik zum Abgleichen des Vertrags und der Implementierung einer Softwareeinheit. Die typische Situation ist folgende: Die Implementierung des Systems lässt Werte oder Zustände zu, die laut Vertrag nicht existieren sollten.

Wenn Sie es so betrachten, können wir darauf hinweisen, dass FP zusätzlich zur Kapselung eine Reihe zusätzlicher Tools bietet, die zu demselben Zweck verwendet werden können:

  1. Unveränderlichkeit als allgegenwärtiger Standard. Sie können transparente Datenwerte an Code von Drittanbietern übergeben. Sie können sie nicht ändern und in ungültige Zustände versetzen. (Karls Antwort macht dies deutlich.)
  2. Anspruchsvolle Typsysteme mit algebraischen Datentypen, mit denen Sie die Struktur Ihrer Typen genau steuern können, ohne viel Code schreiben zu müssen. Wenn Sie diese Einrichtungen mit Bedacht einsetzen, können Sie häufig Typen entwerfen, bei denen "schlechte Zustände" einfach unmöglich sind. (Slogan: "Machen Sie illegale Zustände nicht darstellbar." ) Anstatt die Menge der zulässigen Zustände einer Klasse durch Kapselung indirekt zu steuern, würde ich dem Compiler lieber nur sagen, was diese Zustände sind, und sie mir garantieren lassen!
  3. Dolmetschermuster, wie bereits erwähnt. Ein Schlüssel zum Entwerfen eines guten abstrakten Syntaxbaumtyps ist:
    • Versuchen Sie, den abstrakten Syntaxbaum-Datentyp so zu gestalten, dass alle Werte "gültig" sind.
    • Andernfalls lässt der Interpreter ungültige Kombinationen explizit erkennen und lehnt sie sauber ab.

Diese F # "Entwerfen mit Typen" -Reihe bietet eine recht anständige Lektüre zu einigen dieser Themen, insbesondere zu # 2. (Hierher kommt der Link "Illegale Zustände nicht darstellbar machen" von oben.) Wenn Sie genau hinsehen, werden Sie feststellen, dass im zweiten Teil gezeigt wird, wie Sie die Kapselung verwenden, um Konstruktoren auszublenden und Clients daran zu hindern, ungültige Instanzen zu erstellen. Wie ich oben schon sagte, es ist Teil des Toolkits!


9

Ich kann es wirklich nicht übertreiben, inwieweit die Veränderbarkeit Probleme in der Software verursacht. Viele der Übungen, die uns in den Kopf schlagen, sind eine Entschädigung für Probleme, die durch die Veränderlichkeit verursacht werden. Wenn Sie die Veränderbarkeit aufheben, brauchen Sie diese Praktiken nicht so sehr.

Wenn Sie Unveränderlichkeit haben, wissen Sie, dass sich Ihre Datenstruktur während der Laufzeit nicht unerwartet unter Ihnen ändert, sodass Sie Ihre eigenen abgeleiteten Datenstrukturen für Ihre eigene Verwendung erstellen können, während Sie Ihrem Programm Funktionen hinzufügen. Die ursprüngliche Datenstruktur muss nichts über diese abgeleiteten Datenstrukturen wissen.

Dies bedeutet, dass Ihre Basisdatenstrukturen in der Regel extrem stabil sind. Je nach Bedarf werden neue Datenstrukturen an den Rändern daraus abgeleitet. Es ist wirklich schwer zu erklären, bis Sie ein umfangreiches Funktionsprogramm erstellt haben. Sie kümmern sich immer weniger um die Privatsphäre und denken immer mehr darüber nach, dauerhafte allgemeine öffentliche Datenstrukturen zu erstellen.


Eine Sache, die ich hinzufügen möchte, ist, dass unveränderliche Variablen dazu führen, dass sich Programmierer an die verteilte und verstreute Datenstruktur halten, wenn überhaupt eine Struktur vorhanden ist. Alle Daten sind so strukturiert, dass sie eine logische Gruppe bilden, die zum einfachen Auffinden und Durchlaufen und nicht zum Transportieren dient. Dies ist ein logischer Fortschritt, den Sie machen werden, wenn Sie genug funktionale Programmierung gemacht haben.
Xephon

8

Clojures Tendenz, nur Hashes und Primitive zu verwenden, ist meiner Meinung nach nicht Teil seines funktionalen Erbes, sondern Teil seines dynamischen Erbes. Ich habe ähnliche Tendenzen in Python und Ruby gesehen (beide objektorientiert, imperativ und dynamisch, obwohl beide Funktionen höherer Ordnung ziemlich gut unterstützen), aber nicht etwa in Haskell (das statisch typisiert, aber rein funktional ist) (mit speziellen Konstrukten, die erforderlich sind, um der Unveränderlichkeit zu entgehen).

Die Frage, die Sie stellen müssen, lautet also nicht, wie funktionale Sprachen mit großen APIs umgehen, sondern wie dynamische Sprachen dies tun. Die Antwort lautet: gute Dokumentation und viele, viele Unit-Tests. Glücklicherweise bieten moderne dynamische Sprachen normalerweise eine sehr gute Unterstützung für beide; Beispielsweise können sowohl Python als auch Clojure die Dokumentation in den Code selbst einbetten und nicht nur Kommentare.


Bei statisch typisierten (rein) funktionalen Sprachen gibt es keine (einfache) Möglichkeit, eine Funktion mit einem Datentyp wie bei der OO-Programmierung auszuführen. Dokumentation ist also sowieso wichtig. Der Punkt ist, dass Sie keine Sprachunterstützung benötigen, um eine Schnittstelle zu definieren.
Scara95

5
@ Scara95 Kannst du erklären, was du mit "eine Funktion mit einem Datentyp tragen" meinst?
Sebastian Redl

6

Einige Funktionssprachen bieten die Möglichkeit, Implementierungsdetails in abstrakten Datentypen und Modulen zu kapseln oder auszublenden .

OCaml verfügt beispielsweise über Module, die durch eine Sammlung benannter abstrakter Typen und Werte definiert sind (insbesondere Funktionen, die mit diesen abstrakten Typen arbeiten). In gewissem Sinne sind Ocamls Module Reifing-APIs. Ocaml hat auch Funktoren, die einige Module in ein anderes umwandeln und so eine generische Programmierung ermöglichen. Module sind also kompositorisch.

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.