Ich habe Schwierigkeiten, Ressourcen zum Schreiben von Programmen in einem funktionalen Stil zu finden. Das am weitesten fortgeschrittene Thema, das ich online finden konnte, war die Verwendung von struktureller Typisierung, um Klassenhierarchien zu reduzieren. Die meisten beschäftigen sich nur mit der Verwendung von map / fold / reduction / etc, um imperative Schleifen zu ersetzen.
Was ich wirklich gerne finden würde, ist eine eingehende Diskussion über die OOP-Implementierung eines nicht trivialen Programms, seine Einschränkungen und wie man es in einem funktionalen Stil umgestaltet. Nicht nur ein Algorithmus oder eine Datenstruktur, sondern etwas mit verschiedenen Rollen und Aspekten - vielleicht ein Videospiel. Übrigens habe ich Real-World Functional Programming von Tomas Petricek gelesen, aber ich möchte noch mehr.
Antworten:
Definition der funktionalen Programmierung
In der Einführung zu The Joy of Clojure heißt es:
Programmierung in Scala 2nd Edition p. 10 hat die folgende Definition:
Wenn wir die erste Definition akzeptieren, müssen Sie nur Ihre Schleifen umdrehen, um Ihren Code "funktionsfähig" zu machen. Die zweite Definition beinhaltet die Unveränderlichkeit.
Erstklassige Funktionen
Stellen Sie sich vor, Sie erhalten derzeit eine Passagierliste von Ihrem Busobjekt und iterieren darüber, wobei Sie das Bankkonto jedes Passagiers um den Betrag des Busfahrpreises verringern. Die funktionale Möglichkeit, diese Aktion auszuführen, besteht darin, eine Methode auf dem Bus zu haben, die möglicherweise forEachPassenger heißt und eine Funktion eines Arguments übernimmt. Dann würde Bus über seine Fahrgäste iterieren, was jedoch am besten erreicht wird, und Ihr Kundencode, der den Fahrpreis berechnet, würde in eine Funktion gestellt und an forEachPassenger weitergeleitet. Voila! Sie verwenden funktionale Programmierung.
Imperativ:
Funktional (unter Verwendung einer anonymen Funktion oder "Lambda" in Scala):
Mehr zuckerhaltige Scala-Version:
Nicht erstklassige Funktionen
Wenn Ihre Sprache keine erstklassigen Funktionen unterstützt, kann dies sehr hässlich werden. In Java 7 oder früher müssen Sie eine "Functional Object" -Schnittstelle wie die folgende bereitstellen:
Dann stellt die Bus-Klasse einen internen Iterator bereit:
Zuletzt übergeben Sie ein anonymes Funktionsobjekt an den Bus:
Mit Java 8 können lokale Variablen im Rahmen einer anonymen Funktion erfasst werden. In früheren Versionen müssen solche Variablen jedoch als final deklariert werden. Um dies zu umgehen, müssen Sie möglicherweise eine MutableReference-Wrapper-Klasse erstellen. Hier ist eine ganzzahlspezifische Klasse, mit der Sie dem obigen Code einen Schleifenzähler hinzufügen können:
Trotz dieser Hässlichkeit ist es manchmal vorteilhaft, komplizierte und wiederholte Logik aus Schleifen zu entfernen, die in Ihrem Programm verteilt sind, indem ein interner Iterator bereitgestellt wird.
Diese Hässlichkeit wurde in Java 8 behoben, aber die Behandlung von überprüften Ausnahmen in einer erstklassigen Funktion ist immer noch sehr hässlich, und Java geht in all seinen Sammlungen immer noch von Mutabilität aus. Was uns zu den anderen Zielen bringt, die oft mit FP verbunden sind:
Unveränderlichkeit
Josh Blochs Punkt 13 lautet "Prefer Immutability". Trotz alltäglicher gegenteiliger Abfälle kann OOP mit unveränderlichen Objekten durchgeführt werden, und dies macht es viel besser. Beispielsweise ist String in Java unveränderlich. StringBuffer, OTOH muss veränderbar sein, um einen unveränderlichen String zu erstellen. Einige Aufgaben, z. B. die Arbeit mit Puffern, erfordern von Natur aus eine gewisse Veränderbarkeit.
Reinheit
Jede Funktion sollte zumindest einprägsam sein - wenn Sie ihr dieselben Eingabeparameter geben (und sie sollte außer den eigentlichen Argumenten keine Eingabe haben), sollte sie jedes Mal dieselbe Ausgabe erzeugen, ohne "Nebenwirkungen" wie das Ändern des globalen Zustands zu verursachen und I auszuführen / O oder Ausnahmen werfen.
Es wurde gesagt, dass in der funktionalen Programmierung "etwas Böses erforderlich ist, um die Arbeit zu erledigen". 100% ige Reinheit ist in der Regel nicht das Ziel. Minimierung von Nebenwirkungen ist.
Fazit
Von allen oben genannten Ideen war die Unveränderlichkeit der größte Gewinn in Bezug auf praktische Anwendungen zur Vereinfachung meines Codes - ob OOP oder FP. Die Weitergabe von Funktionen an Iteratoren ist der zweitgrößte Gewinn. Die Dokumentation zu Java 8 Lambdas bietet die beste Erklärung dafür. Rekursion ist ideal für die Verarbeitung von Bäumen. Mit Laziness können Sie mit unendlichen Sammlungen arbeiten.
Wenn Sie die JVM mögen, empfehle ich Ihnen einen Blick auf Scala und Clojure. Beide sind aufschlussreiche Interpretationen der funktionalen Programmierung. Scala ist typsicher mit etwas C-ähnlicher Syntax, obwohl es mit Haskell wirklich so viel Syntax gemeinsam hat wie mit C. Clojure ist nicht typsicher und es ist ein Lisp. Ich habe kürzlich einen Vergleich von Java, Scala und Clojure in Bezug auf ein bestimmtes Refactoring-Problem veröffentlicht. Logan Campbells Vergleich mit dem Game of Life beinhaltet auch Haskell und Clojure.
PS
Jimmy Hoffa wies darauf hin, dass meine Busklasse veränderlich ist. Anstatt das Original zu reparieren, denke ich, wird dies genau die Art der Umgestaltung demonstrieren, um die es in dieser Frage geht. Dies kann behoben werden, indem jede Methode im Bus zu einer Fabrik gemacht wird, um einen neuen Bus zu produzieren, und jede Methode im Passagier zu einer Fabrik, um einen neuen Passagier zu produzieren. Daher habe ich zu allem einen Rückgabetyp hinzugefügt, was bedeutet, dass ich die java.util.function.Function von Java 8 anstelle der Consumer-Schnittstelle kopiere:
Dann im Bus:
Schließlich gibt das anonyme Funktionsobjekt den geänderten Zustand der Dinge zurück (einen neuen Bus mit neuen Fahrgästen). Dies setzt voraus, dass p.debit () jetzt einen neuen unveränderlichen Passagier mit weniger Geld als das Original zurückgibt:
Hoffentlich können Sie jetzt selbst entscheiden, wie funktional Ihre imperative Sprache sein soll, und entscheiden, ob es besser ist, Ihr Projekt mit einer funktionalen Sprache neu zu gestalten. In Scala oder Clojure wurden Sammlungen und andere APIs entwickelt, um die funktionale Programmierung zu vereinfachen. Beide haben ein sehr gutes Java-Interop, sodass Sie Sprachen mischen und zuordnen können. Aus Gründen der Java-Interoperabilität kompiliert Scala seine First-Class-Funktionen in anonyme Klassen, die mit den Java 8-Funktionsschnittstellen nahezu kompatibel sind. Über die Details können Sie in der Scala in der Sektion Tiefe lesen . 1.3.2 .
quelle
Ich habe persönliche Erfahrung damit. Am Ende habe ich nichts gefunden, was rein funktional ist, aber ich habe etwas gefunden, mit dem ich zufrieden bin. So habe ich es gemacht:
x
, stellen Sie dies so ein, dass die Methode übergeben wird,x
anstatt aufzurufenthis.x
.x.methodThatModifiesTheFooVar()
infooFn(x.foo)
map
,reduce
,filter
usw.Ich konnte den veränderlichen Zustand nicht loswerden. In meiner Sprache (JavaScript) war es einfach zu nicht idiomatisch. Indem alle Zustände übergeben und / oder zurückgegeben werden, kann jede Funktion getestet werden. Dies unterscheidet sich von OOP, bei dem das Einrichten des Status zu lange dauern würde oder das Trennen von Abhängigkeiten häufig zuerst das Ändern des Produktionscodes erfordert.
Ich könnte mich auch bei der Definition irren, denke aber, dass meine Funktionen referenziell transparent sind: Meine Funktionen haben bei gleicher Eingabe den gleichen Effekt.
Bearbeiten
Wie Sie hier sehen können , ist es nicht möglich, ein wirklich unveränderliches Objekt in JavaScript zu erstellen. Wenn Sie fleißig sind und die Kontrolle darüber haben, wer Ihren Code aufruft, können Sie dies tun, indem Sie immer ein neues Objekt erstellen, anstatt das aktuelle zu ändern. Die Mühe hat sich für mich nicht gelohnt.
Wenn Sie Java verwenden , können Sie diese Techniken verwenden , um Ihre Klassen unveränderlich zu machen.
quelle
Ich denke nicht, dass es wirklich möglich ist, das Programm komplett umzugestalten - Sie müssten das richtige Paradigma umgestalten und neu implementieren.
Ich habe gesehen, dass Code-Refactoring als "disziplinierte Technik zur Umstrukturierung eines vorhandenen Code-Körpers definiert ist, die seine interne Struktur ändert, ohne sein externes Verhalten zu ändern".
Sie könnten bestimmte Dinge funktionaler machen, aber im Kern haben Sie immer noch ein objektorientiertes Programm. Sie können nicht einfach kleine Teile ändern, um sie an ein anderes Paradigma anzupassen.
quelle
Ich denke, diese Artikelserie ist genau das, was Sie wollen:
Rein funktionale Retrogames
http://prog21.dadgum.com/23.html Teil 1
http://prog21.dadgum.com/24.html Teil 2
http://prog21.dadgum.com/25.html Teil 3
http://prog21.dadgum.com/26.html Teil 4
http://prog21.dadgum.com/37.html Follow-up
Die Zusammenfassung lautet:
Der Autor schlägt eine Hauptschleife mit Nebenwirkungen vor (Nebenwirkungen müssen irgendwo auftreten, oder?) Und die meisten Funktionen geben kleine unveränderliche Datensätze zurück, die genau beschreiben, wie sie den Status des Spiels verändert haben.
Natürlich werden Sie beim Schreiben eines realen Programms mehrere Programmierstile mischen und aufeinander abstimmen, wobei jeder dort verwendet wird, wo es am meisten hilft. Es ist jedoch eine gute Lernerfahrung, zu versuchen, ein Programm auf die funktionalste / unveränderlichste Weise zu schreiben und es auch auf die spaghettieste Weise zu schreiben, wobei nur globale Variablen verwendet werden :-) (Machen Sie es als Experiment, bitte nicht in Produktion)
quelle
Sie müssten wahrscheinlich Ihren gesamten Code auf den Kopf stellen, da OOP und FP beim Organisieren von Code zwei unterschiedliche Ansätze verfolgen.
OOP organisiert Code um Typen (Klassen): Verschiedene Klassen können dieselbe Operation implementieren (eine Methode mit derselben Signatur). Infolgedessen ist OOP besser geeignet, wenn sich die Menge der Vorgänge nicht wesentlich ändert, während sehr oft neue Typen hinzugefügt werden können. Betrachten wir zum Beispiel eine GUI - Bibliothek , in der jedes Widget einen festen Satz von Methoden aufweist (
hide()
,show()
,paint()
,move()
, und so weiter) , aber neue Widgets hinzugefügt werden könnte , wie die Bibliothek ausgefahren ist. In OOP ist es einfach, einen neuen Typ hinzuzufügen (für eine bestimmte Schnittstelle): Sie müssen nur eine neue Klasse hinzufügen und alle Methoden implementieren (lokale Codeänderung). Andererseits kann das Hinzufügen einer neuen Operation (Methode) zu einer Schnittstelle das Ändern aller Klassen erfordern, die diese Schnittstelle implementieren (obwohl die Vererbung den Arbeitsaufwand verringern kann).FP organisiert Code um Operationen (Funktionen): Jede Funktion implementiert eine Operation, die unterschiedliche Typen auf unterschiedliche Weise behandeln kann. Dies wird normalerweise erreicht, indem der Typ über einen Mustervergleich oder einen anderen Mechanismus abgesetzt wird. Infolgedessen ist FP besser geeignet, wenn die Menge der Typen stabil ist und häufiger neue Operationen hinzugefügt werden. Nehmen Sie zum Beispiel einen festen Satz von Bildformaten (GIF, JPEG usw.) und einige Algorithmen, die Sie implementieren möchten. Jeder Algorithmus kann durch eine Funktion implementiert werden, die sich je nach Art des Bildes unterschiedlich verhält. Das Hinzufügen eines neuen Algorithmus ist einfach, da Sie nur eine neue Funktion implementieren müssen (lokale Codeänderung). Um ein neues Format (Typ) hinzuzufügen, müssen Sie alle bisher implementierten Funktionen ändern, um es zu unterstützen (nicht lokale Änderung).
Fazit: OOP und FP unterscheiden sich grundlegend in der Art und Weise, wie sie Code organisieren. Wenn Sie ein OOP-Design in ein FP-Design umwandeln, müssen Sie den gesamten Code entsprechend anpassen. Es kann jedoch eine interessante Übung sein. Siehe auch diese Vorlesungsunterlagen zu dem von mikemay zitierten SICP-Buch, insbesondere die Folien 13.1.5 bis 13.1.10.
quelle