Wie gehen rein funktionale Sprachen mit Modularität um?

23

Ich komme aus einem objektorientierten Hintergrund, in dem ich gelernt habe, dass Klassen verwendet werden oder zumindest verwendet werden können, um eine Abstraktionsebene zu erstellen, die ein einfaches Recycling von Code ermöglicht, der dann entweder zum Erstellen von Objekten oder zur Vererbung verwendet werden kann.

Wie zum Beispiel kann ich eine Tierklasse haben und dann Katzen und Hunde erben und so, dass alle viele der gleichen Merkmale erben, und aus diesen Unterklassen kann ich dann Objekte machen, die eine Tierrasse oder sogar den Namen spezifizieren können davon.
Oder ich kann Klassen verwenden, um mehrere Instanzen desselben Codes anzugeben, der leicht unterschiedliche Dinge behandelt oder enthält. wie Knoten in einem Suchbaum oder mehrere verschiedene Datenbankverbindungen und was nicht.

Ich bin vor kurzem zur funktionalen Programmierung übergegangen und begann mich zu fragen:
Wie gehen rein funktionale Sprachen mit solchen Dingen um? Das heißt, Sprachen ohne Konzept von Klassen und Objekten.

Elektrischer Kaffee
quelle
1
Warum bedeutet funktional Ihrer Meinung nach kein Unterricht? Einige der ersten Klassen stammten von LISP- CLOS. Schauen Sie sich Clojure- Namespaces und -Typen oder -Module und -Haskell an .
Ich bezog mich auf die funktionalen Sprachen, die KEINEN Unterricht haben, ich bin mir der wenigen sehr wohl bewusst, die DO
Electric Coffee am
1
Caml als Beispiel, seine Schwestersprache OCaml, fügt Objekte hinzu, aber Caml selbst hat sie nicht.
Electric Coffee
12
Der Begriff "rein funktional" bezieht sich auf funktionale Sprachen, die referenzielle Transparenz aufrechterhalten, und hängt nicht davon ab, ob die Sprache objektorientierte Merkmale aufweist oder nicht.
2.
2
Der Kuchen ist eine Lüge, die Wiederverwendung von Code in OO ist weitaus schwieriger als in FP. Trotz allem, was OO im Laufe der Jahre für die Wiederverwendung von Code beansprucht hat, habe ich gesehen, dass dies ein Minimum an Zeiten durchlief. (frei fühlen nur sagen , ich muss es falsch machen, ich fühle mich wohl, wie gut ich schreibe OO - Code gehabt zu haben OO - Systeme seit Jahren zu entwerfen und zu pflegen, ich kenne die Qualität meiner eigenen Ergebnisse)
Jimmy Hoffa

Antworten:

19

Viele Funktionssprachen haben ein Modulsystem. (Übrigens auch viele objektorientierte Sprachen.) Aber auch ohne eine können Sie Funktionen als Module verwenden.

JavaScript ist ein gutes Beispiel. In JavaScript werden Funktionen sowohl zur Implementierung von Modulen als auch zur objektorientierten Kapselung verwendet. In Schema, das die Hauptinspiration für JavaScript war, gibt es nur Funktionen. Funktionen werden verwendet, um fast alles zu implementieren: Objekte, Module ( im Racket Einheiten genannt ), sogar Datenstrukturen.

OTOH, Haskell und die ML-Familie haben ein explizites Modulsystem.

Bei der Objektorientierung geht es um Datenabstraktion. Das ist es. Modularität, Vererbung, Polymorphismus und sogar veränderlicher Zustand sind orthogonale Bedenken.

Jörg W. Mittag
quelle
8
Können Sie erklären, wie diese Dinge in Bezug auf oop ein bisschen detaillierter funktionieren? Anstatt nur zu behaupten, dass die Konzepte existieren ...
Electric Coffee
Nebenbemerkung - Module und Units sind zwei verschiedene Konstrukte in Racket - Module sind vergleichbar mit Namespaces, und Units befinden sich auf halbem Weg zwischen Namespaces und OO-Interfaces. In den Unterlagen werden die Unterschiede viel detaillierter beschrieben
Jack
@ Jack: Ich wusste nicht, dass Racket auch ein Konzept namens hat module. Ich finde es bedauerlich, dass Racket ein Konzept hat, moduledas nicht als Modul bezeichnet wird, und ein Konzept, das ein Modul ist, das aber nicht genannt wird module. Wie auch immer, Sie haben geschrieben: "Einheiten befinden sich auf halbem Weg zwischen Namespaces und OO-Schnittstellen". Ist das nicht die Definition eines Moduls?
Jörg W Mittag
Module und Einheiten sind beide Gruppen von Namen, die an Werte gebunden sind. Module können Abhängigkeiten von anderen spezifischen Bindungssätzen haben, während Einheiten Abhängigkeiten von einigen allgemeinen Bindungssätzen haben können , die jeder andere Code, der die Einheit verwendet, bereitstellen muss. Einheiten werden über Bindungen parametrisiert , Module nicht. Ein Modul, das von einer Bindung abhängig ist, mapund eine Einheit, die von einer Bindung abhängig mapist, unterscheiden sich darin, dass sich das Modul auf eine bestimmte Bindung beziehen mussmap , z. B. die von racket/base, während verschiedene Benutzer der Einheit der Einheit unterschiedliche Definitionen geben können map.
Jack
4

Anscheinend stellen Sie zwei Fragen: "Wie können Sie Modularität in funktionalen Sprachen erreichen?" Was wurde in anderen Antworten behandelt und "Wie kann man Abstraktionen in funktionalen Sprachen erstellen?" was ich antworten werde.

In OO-Sprachen tendieren Sie dazu, sich auf das Substantiv "ein Tier", "den Mailserver", "seine Gartengabel" usw. zu konzentrieren. Funktionale Sprachen betonen dagegen das Verb "laufen", "Post holen". , "anstoßen", etc.

Es ist daher keine Überraschung, dass Abstraktionen in funktionalen Sprachen eher über Verben oder Operationen als über Dingen liegen. Ein Beispiel, nach dem ich immer greife, wenn ich versuche, dies zu erklären, ist das Parsen. In funktionalen Sprachen können Sie Parser am besten schreiben, indem Sie eine Grammatik angeben und dann interpretieren. Der Interpreter erzeugt eine Abstraktion über den Parsing-Prozess.

Ein weiteres konkretes Beispiel hierfür ist ein Projekt, an dem ich vor kurzem gearbeitet habe. Ich habe eine Datenbank in Haskell geschrieben. Ich hatte eine eingebettete Sprache, um Operationen auf der untersten Ebene zu spezifizieren. So konnte ich beispielsweise Daten auf dem Speichermedium schreiben und lesen. Ich hatte eine andere, separate, eingebettete Sprache, um Operationen auf höchster Ebene zu spezifizieren. Dann hatte ich, was im Grunde genommen ein Dolmetscher ist, die Möglichkeit, Operationen von der höheren in die niedrigere Ebene umzuwandeln.

Dies ist eine bemerkenswert allgemeine Form der Abstraktion, aber nicht die einzige, die in funktionalen Sprachen verfügbar ist.

dan_waterworth
quelle
4

Obwohl "funktionale Programmierung" keine weitreichenden Auswirkungen auf Fragen der Modularität hat, wenden sich bestimmte Sprachen auf unterschiedliche Weise an die allgemeine Programmierung. Wiederverwendung und Abstraktion von Code interagieren dahingehend, dass es umso schwieriger ist, den Code wiederzuverwenden, je weniger Sie exponieren. Abgesehen von der Abstraktion werde ich auf zwei Aspekte der Wiederverwendbarkeit eingehen.

Statisch typisierte OOP-Sprachen verwenden traditionell nominelle Untertypen, was bedeutet, dass ein für Klasse / Modul / Schnittstelle A entwickelter Code nur dann Klasse / Modul / Schnittstelle B behandeln kann, wenn B A explizit erwähnt Dieser Code für A kann B verarbeiten, wenn B über alle Methoden und / oder Felder von A verfügt. B hätte von einem anderen Team erstellt werden können, bevor eine allgemeinere Klasse / Schnittstelle A benötigt wurde. Zum Beispiel in OCaml, strukturelle Untertypisierung Dies gilt für das Modulsystem, das OOP-ähnliche Objektsystem und seine ganz eigenen polymorphen Variantentypen.

Der auffälligste Unterschied zwischen OOP und FP wrt. Modularität ist, dass die Standard- "Einheit" in OOP als Objekt verschiedene Operationen für denselben Wertefall bündelt, während die Standard- "Einheit" in FP als Funktion dieselbe Operation für verschiedene Wertefälle bündelt. In FP ist es immer noch sehr einfach, Operationen zu bündeln, beispielsweise als Module. (Übrigens haben weder Haskell noch F # ein vollwertiges Modulsystem der ML-Familie.) Das Ausdrucksproblemist die Aufgabe, schrittweise neue Operationen hinzuzufügen, die an allen Werten arbeiten (z. B. eine neue Methode an vorhandene Objekte anhängen), und neue Fälle von Werten, die von allen Operationen unterstützt werden sollen (z. B. das Hinzufügen einer neuen Klasse mit derselben Schnittstelle). Wie in der folgenden ersten Vorlesung von Ralf Laemmel (mit ausführlichen Beispielen in C #) erläutert, ist das Hinzufügen neuer Operationen in OOP-Sprachen problematisch.

Die Kombination von OOP und FP in Scala könnte es zu einer der mächtigsten Sprachen machen. Modularität. Aber OCaml ist immer noch meine Lieblingssprache und meiner persönlichen, subjektiven Meinung nach nicht weniger als Scala. In den beiden folgenden Vorlesungen von Ralf Laemmel wird die Lösung des Ausdrucksproblems in Haskell erörtert. Ich denke, dass diese Lösung, obwohl sie perfekt funktioniert, es schwierig macht, die resultierenden Daten mit parametrischem Polymorphismus zu verwenden. Das Lösen des Expressionsproblems mit polymorphen Varianten in OCaml, das in dem unten verlinkten Jaques Garrigue-Artikel erläutert wird, weist diesen Nachteil nicht auf. Ich verweise auch auf Lehrbuchkapitel, in denen die Verwendung von Nicht-OOP- und OOP-Modularität in OCaml verglichen wird.

Nachfolgend finden Sie Haskell- und OCaml-spezifische Links, die das Ausdrucksproblem erläutern :

Lukstafi
quelle
2
Würde es Ihnen etwas ausmachen, näher zu erläutern, was diese Ressourcen bewirken und warum Sie diese zur Beantwortung der gestellten Frage empfehlen? "Nur-Link-Antworten" sind bei Stack Exchange nicht ganz willkommen
gnat
2
Ich habe nur eine tatsächliche Antwort und nicht nur Links als Bearbeitung bereitgestellt.
Lukstafi
0

Tatsächlich ist OO-Code weitaus weniger wiederverwendbar, und das ist beabsichtigt. Die Idee hinter OOP ist, Operationen auf bestimmte Datenelemente auf bestimmten privilegierten Code zu beschränken, der sich entweder in der Klasse oder an der entsprechenden Stelle in der Vererbungshierarchie befindet. Dies begrenzt die nachteiligen Auswirkungen der Veränderlichkeit. Wenn sich eine Datenstruktur ändert, gibt es nur so viele Stellen im Code, die verantwortlich sein können.

Aufgrund der Unveränderlichkeit ist es Ihnen egal, wer mit einer bestimmten Datenstruktur arbeiten kann, da niemand Ihre Kopie der Daten ändern kann. Dies erleichtert das Erstellen neuer Funktionen zum Bearbeiten vorhandener Datenstrukturen erheblich. Sie erstellen einfach die Funktionen und gruppieren sie in Module, die aus Domain-Sicht angemessen erscheinen. Sie müssen sich keine Gedanken darüber machen, wo sie in die Vererbungshierarchie eingefügt werden sollen.

Die andere Art der Wiederverwendung von Code besteht darin, neue Datenstrukturen zu erstellen, um an vorhandenen Funktionen zu arbeiten. Dies wird in funktionalen Sprachen mithilfe von Funktionen wie Generika und Typklassen erledigt. Mit der Ord- Typ-Klasse von Haskell können Sie die sortFunktion beispielsweise für jeden Typ mit einer OrdInstanz verwenden. Instanzen können einfach erstellt werden, wenn sie noch nicht vorhanden sind.

Nehmen Sie Ihr AnimalBeispiel und überlegen Sie, eine Fütterungsfunktion zu implementieren. Die einfache OOP-Implementierung besteht darin, eine Sammlung von AnimalObjekten zu verwalten und alle zu durchlaufen, wobei die feedMethode für jedes Objekt aufgerufen wird .

Allerdings wird es schwierig, wenn es um Details geht. Ein AnimalObjekt weiß natürlich, welche Art von Nahrung es isst und wie viel es braucht, um sich satt zu fühlen. Es ist natürlich nicht bekannt, wo das Lebensmittel aufbewahrt wird und wie viel verfügbar ist, so dass ein FoodStoreObjekt zu einer Abhängigkeit von jedem geworden ist Animal, entweder als Feld des AnimalObjekts oder als Parameter der feedMethode übergeben. Um die AnimalKohäsion der Klasse zu erhalten, können Sie auch feed(animal)zum FoodStoreObjekt wechseln oder einen Gräuel für eine Klasse erstellen, die als eine AnimalFeederoder eine solche bezeichnet wird.

In FP gibt es keine Neigung für die Felder von und Animal, immer gruppiert zu bleiben, was einige interessante Implikationen für die Wiederverwendbarkeit hat. Sagen Sie bitte eine Liste haben AnimalDatensätze, mit Feldern wie name, species, location, food type, food amount, etc. Sie haben auch eine Liste von FoodStoreDatensätzen mit Feldern wie location, food typeund food amount.

Der erste Schritt beim Füttern könnte darin bestehen, jede dieser Listen von Aufzeichnungen auf Listen von (food amount, food type)Paaren abzubilden , mit negativen Zahlen für die Tiermengen. Sie können dann Funktionen erstellen, mit denen Sie mit diesen Paaren alle möglichen Aufgaben ausführen können, z. B. die Summe der Mengen der einzelnen Arten von Lebensmitteln. Diese Funktionen gehören weder zu einem Animalnoch zu einem FoodStoreModul, können aber von beiden in hohem Maße wiederverwendet werden.

Am Ende stehen Ihnen eine Reihe von Funktionen zur Verfügung, die [(Num A, Eq B)]wiederverwendbar und modular sind. Sie können jedoch nur schwer herausfinden, wo Sie sie platzieren oder wie Sie sie als Gruppe bezeichnen sollen. Der Effekt ist, dass FP-Module schwieriger zu klassifizieren sind, die Klassifizierung jedoch weniger wichtig ist.

Karl Bielefeldt
quelle
-1

Eine der gängigsten Lösungen ist das Aufteilen von Code in Module. Dies geschieht in JavaScript folgendermaßen:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

Der vollständige Artikel erklärt dieses Muster in JavaScript . Abgesehen davon gibt es eine Reihe anderer Möglichkeiten, ein Modul zu definieren, z. B. RequireJS , CommonJS , Google Closure. Ein anderes Beispiel ist Erlang, wo Sie sowohl Module als auch Verhalten haben , die API und Muster erzwingen und eine ähnliche Rolle spielen wie Schnittstellen in OOP.

David Sergey
quelle