Definizione di programmazione funzionale
L'introduzione a La gioia di Clojure dice quanto segue:
La programmazione funzionale è uno di quei termini informatici che ha una definizione amorfa. Se chiedi a 100 programmatori la loro definizione, probabilmente riceverai 100 risposte diverse ...
La programmazione funzionale riguarda e facilita l'applicazione e la composizione delle funzioni ... Perché un linguaggio sia considerato funzionale, la sua nozione di funzione deve essere di prima classe. Le funzioni di prima classe possono essere archiviate, passate e restituite come qualsiasi altro dato. Al di là di questo concetto chiave, [le definizioni di FP potrebbero includere] purezza, immutabilità, ricorsione, pigrizia e trasparenza referenziale.
Programmazione in Scala 2a edizione p. 10 ha la seguente definizione:
La programmazione funzionale è guidata da due idee principali. La prima idea è che le funzioni sono valori di prima classe ... Puoi passare funzioni come argomenti ad altre funzioni, restituirle come risultati da funzioni o memorizzarle in variabili ...
La seconda idea principale della programmazione funzionale è che le operazioni di un programma dovrebbero mappare i valori di input sui valori di output anziché modificare i dati in atto.
Se accettiamo la prima definizione, l'unica cosa che devi fare per rendere il tuo codice "funzionale" è capovolgere i tuoi loop. La seconda definizione include l'immutabilità.
Funzioni di prima classe
Immagina di ottenere attualmente un Elenco di passeggeri dal tuo oggetto Bus e di iterare su di esso diminuendo il conto bancario di ciascun passeggero in base all'importo della tariffa del bus. Il modo funzionale per eseguire questa stessa azione sarebbe di avere un metodo su Bus, forse chiamato forEachPassenger che accetta una funzione di un argomento. Quindi Bus verrebbe ripetuto sui propri passeggeri, tuttavia ciò è meglio realizzato e il codice cliente che addebita la tariffa per la corsa verrebbe messo in funzione e passato a per ogni passeggero. Ecco! Stai utilizzando la programmazione funzionale.
Imperativo:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Funzionale (usando una funzione anonima o "lambda" in Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Versione scala più zuccherina:
myBus = myBus.forEachPassenger(_.debit(fare))
Funzioni non di prima classe
Se la tua lingua non supporta funzioni di prima classe, questo può diventare molto brutto. In Java 7 o versioni precedenti, devi fornire un'interfaccia "Oggetto funzionale" come questa:
// 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);
}
Quindi la classe Bus fornisce un iteratore interno:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Infine, si passa un oggetto funzione anonimo al 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);
}
}
Java 8 consente alle variabili locali di acquisire l'ambito di una funzione anonima, ma nelle versioni precedenti, tali varianti devono essere dichiarate definitive. Per ovviare a questo problema potrebbe essere necessario creare una classe wrapper MutableReference. Ecco una classe specifica per intero che ti consente di aggiungere un contatore di cicli al codice sopra:
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());
Anche con questa bruttezza, a volte è utile eliminare la logica complicata e ripetuta dai loop diffusi nel programma fornendo un iteratore interno.
Questa bruttezza è stata risolta in Java 8, ma la gestione delle eccezioni verificate all'interno di una funzione di prima classe è ancora davvero brutta e Java porta ancora il presupposto della mutabilità in tutte le sue raccolte. Il che ci porta agli altri obiettivi spesso associati a FP:
Immutabilità
L'articolo 13 di Josh Bloch è "Preferisci l'immutabilità". Nonostante i discorsi comuni della spazzatura al contrario, OOP può essere fatto con oggetti immutabili, e così facendo lo rende molto meglio. Ad esempio, String in Java è immutabile. StringBuffer, OTOH deve essere modificabile per creare una stringa immutabile. Alcune attività, come lavorare con i buffer, richiedono intrinsecamente la mutabilità.
Purezza
Ogni funzione dovrebbe almeno essere memorizzabile - se le dai gli stessi parametri di input (e non dovrebbe avere input oltre ai suoi argomenti reali), dovrebbe produrre lo stesso output ogni volta senza causare "effetti collaterali" come cambiare lo stato globale, eseguendo I / O, o generando eccezioni.
È stato detto che nella Programmazione Funzionale "di solito è necessario un po 'di male per portare a termine il lavoro". 100% di purezza non è generalmente l'obiettivo. Ridurre al minimo gli effetti collaterali è.
Conclusione
In realtà, tra tutte le idee sopra, l'immutabilità è stata la più grande vittoria singola in termini di applicazioni pratiche per semplificare il mio codice - OOP o FP. Passare le funzioni agli iteratori è la seconda vittoria più grande. La documentazione di Java 8 Lambdas ha la migliore spiegazione del perché. La ricorsione è ottima per l'elaborazione degli alberi. La pigrizia ti consente di lavorare con infinite collezioni.
Se ti piace la JVM, ti consiglio di dare un'occhiata a Scala e Clojure. Entrambe sono interpretazioni approfondite della programmazione funzionale. Scala è sicuro per i tipi con una sintassi un po 'tipo C, sebbene abbia davvero la stessa sintassi in comune con Haskell come con C. Clojure non è sicuro per i tipi ed è un Lisp. Di recente ho pubblicato un confronto tra Java, Scala e Clojure per quanto riguarda uno specifico problema di refactoring. Il confronto tra Logan Campbell usando il Gioco della vita include anche Haskell e il tipo Clojure.
PS
Jimmy Hoffa ha sottolineato che la mia classe di autobus è mutevole. Piuttosto che correggere l'originale, penso che ciò dimostrerà esattamente il tipo di refactoring di cui questa domanda si occupa. Questo può essere risolto rendendo ogni metodo su Bus una fabbrica per produrre un nuovo Bus, ogni metodo su Passeggero una fabbrica per produrre un nuovo Passeggero. Quindi ho aggiunto un tipo restituito a tutto ciò che significa che copierò java.util.function.Function di Java 8 invece dell'interfaccia Consumer:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Quindi sul 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));
}
Infine, l'oggetto funzione anonimo restituisce lo stato delle cose modificato (un nuovo autobus con nuovi passeggeri). Ciò presuppone che p.debit () ora restituisca un nuovo passeggero immutabile con meno soldi dell'originale:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
Spero che ora tu possa prendere la tua decisione su quanto funzionale vuoi rendere il tuo linguaggio imperativo e decidere se sarebbe meglio ridisegnare il tuo progetto usando un linguaggio funzionale. In Scala o Clojure, le collezioni e le altre API sono progettate per semplificare la programmazione funzionale. Entrambi hanno un'ottima interoperabilità Java, quindi puoi mescolare e abbinare le lingue. In effetti, per l'interoperabilità di Java, Scala compila le sue funzioni di prima classe in classi anonime che sono quasi compatibili con le interfacce funzionali di Java 8. Puoi leggere i dettagli in Scala nella setta Depth. 1.3.2 .