Definition der funktionalen Programmierung
In der Einführung zu The Joy of Clojure heißt es:
Funktionale Programmierung ist einer der Computerausdrücke, die eine amorphe Definition haben. Wenn Sie 100 Programmierer nach ihrer Definition fragen, erhalten Sie wahrscheinlich 100 verschiedene Antworten ...
Funktionale Programmierung betrifft und erleichtert die Anwendung und Zusammensetzung von Funktionen ... Damit eine Sprache als funktional angesehen werden kann, muss ihr Funktionsbegriff erstklassig sein. Erstklassige Funktionen können wie alle anderen Daten gespeichert, übergeben und zurückgegeben werden. Über dieses Kernkonzept hinaus können [Definitionen von FP] Reinheit, Unveränderlichkeit, Rekursion, Faulheit und referenzielle Transparenz umfassen.
Programmierung in Scala 2nd Edition p. 10 hat die folgende Definition:
Die funktionale Programmierung basiert auf zwei Hauptideen. Die erste Idee ist, dass Funktionen erstklassige Werte sind ... Sie können Funktionen als Argumente an andere Funktionen übergeben, sie als Ergebnisse von Funktionen zurückgeben oder sie in Variablen speichern ...
Die zweite Hauptidee der funktionalen Programmierung besteht darin, dass die Operationen eines Programms Eingabewerte auf Ausgabewerte abbilden sollten, anstatt Daten an Ort und Stelle zu ändern.
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:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Funktional (unter Verwendung einer anonymen Funktion oder "Lambda" in Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Mehr zuckerhaltige Scala-Version:
myBus = myBus.forEachPassenger(_.debit(fare))
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:
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
Dann stellt die Bus-Klasse einen internen Iterator bereit:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Zuletzt übergeben Sie ein anonymes Funktionsobjekt an den Bus:
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
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:
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
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:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Dann im Bus:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
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:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
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 .