Definicja programowania funkcjonalnego
Wstęp do Radości Clojure mówi:
Programowanie funkcjonalne jest jednym z tych terminów komputerowych, które mają amorficzną definicję. Jeśli poprosisz 100 programistów o ich definicję, prawdopodobnie otrzymasz 100 różnych odpowiedzi ...
Programowanie funkcjonalne dotyczy i ułatwia stosowanie i układ funkcji ... Aby język można było uznać za funkcjonalny, jego pojęcie funkcji musi być najwyższej klasy. Funkcje pierwszej klasy mogą być przechowywane, przekazywane i zwracane tak jak każdy inny element danych. Oprócz tej podstawowej koncepcji [definicje FP mogą obejmować] czystość, niezmienność, rekurencję, lenistwo i przejrzystość referencyjną.
Programowanie w Scali Wydanie 2 s. 10 ma następującą definicję:
Programowanie funkcjonalne opiera się na dwóch głównych pomysłach. Pierwszym pomysłem jest to, że funkcje są pierwszorzędnymi wartościami ... Możesz przekazywać funkcje jako argumenty do innych funkcji, zwracać je jako wyniki funkcji lub przechowywać w zmiennych ...
Drugą główną ideą programowania funkcjonalnego jest to, że operacje programu powinny mapować wartości wejściowe na wartości wyjściowe, a nie zmieniać dane w miejscu.
Jeśli zaakceptujemy pierwszą definicję, jedyną rzeczą, którą musisz zrobić, aby Twój kod był „funkcjonalny”, jest wywrócenie pętli na lewą stronę. Druga definicja obejmuje niezmienność.
Funkcje pierwszej klasy
Wyobraź sobie, że obecnie otrzymujesz Listę Pasażerów z obiektu Autobus i iterujesz nad nią, zmniejszając rachunek bankowy każdego pasażera o kwotę opłaty za autobus. Funkcjonalnym sposobem wykonania tej samej akcji byłoby posiadanie metody na magistrali, być może nazywanej forEachPassenger, która przyjmuje funkcję jednego argumentu. Wówczas Bus będzie iterował swoich pasażerów, ale najlepiej to osiągnąć, a kod klienta, który nalicza opłatę za przejazd, zostałby włączony w funkcję i przekazany do forEachPassenger. Voila! Używasz programowania funkcjonalnego.
Tryb rozkazujący:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Funkcjonalny (przy użyciu anonimowej funkcji lub „lambda” w Scali):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Bardziej słodka wersja Scali:
myBus = myBus.forEachPassenger(_.debit(fare))
Funkcje nie pierwszej klasy
Jeśli twój język nie obsługuje pierwszorzędnych funkcji, może to być bardzo brzydkie. W Javie 7 lub wcześniejszej musisz udostępnić interfejs „Obiekt funkcjonalny” w następujący sposób:
// 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);
}
Następnie klasa Bus zapewnia wewnętrzny iterator:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Na koniec przekazujesz anonimowy obiekt funkcji do magistrali:
// 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 umożliwia przechwytywanie zmiennych lokalnych w zakresie funkcji anonimowej, ale we wcześniejszych wersjach wszelkie takie zmienne muszą być deklarowane jako ostateczne. Aby obejść ten problem, może być konieczne utworzenie klasy opakowania MutableReference. Oto klasa specyficzna dla liczb całkowitych, która pozwala dodać licznik pętli do powyższego kodu:
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());
Nawet przy tej brzydocie czasem korzystne jest wyeliminowanie skomplikowanej i powtarzalnej logiki z pętli rozproszonych w całym programie poprzez zapewnienie wewnętrznego iteratora.
Ta brzydota została naprawiona w Javie 8, ale obsługa sprawdzonych wyjątków wewnątrz funkcji pierwszej klasy jest nadal bardzo brzydka, a Java nadal zakłada założenie zmienności we wszystkich swoich kolekcjach. Co prowadzi nas do innych celów często związanych z FP:
Niezmienność
Przedmiotem 13 Josha Blocha jest „Preferuj niezmienność”. Mimo zwykłych śmieci mówi się inaczej, OOP można zrobić z niezmiennymi obiektami, a to czyni go znacznie lepszym. Na przykład String w Javie jest niezmienny. StringBuffer, OTOH musi być modyfikowalny, aby zbudować niezmienny ciąg. Niektóre zadania, takie jak praca z buforami, z natury wymagają modyfikacji.
Czystość
Każda funkcja powinna przynajmniej być zapamiętywalna - jeśli podasz jej te same parametry wejściowe (i nie powinna mieć żadnych danych wejściowych oprócz faktycznych argumentów), powinna generować to samo wyjście za każdym razem bez powodowania „efektów ubocznych”, takich jak zmiana stanu globalnego, wykonanie I / O lub zgłaszanie wyjątków.
Powiedziano, że w Programowaniu Funkcjonalnym „zwykle zło jest potrzebne do wykonania pracy”. 100% czystości na ogół nie jest celem. Minimalizacja skutków ubocznych to.
Wniosek
Naprawdę, ze wszystkich powyższych pomysłów, niezmienność była największą pojedynczą wygraną pod względem praktycznych zastosowań dla uproszczenia mojego kodu - czy to OOP, czy FP. Przekazywanie funkcji iteratorom to druga największa wygrana. Dokumentacja Java 8 Lambdas zawiera najlepsze wyjaśnienie, dlaczego. Rekurencja jest świetna do przetwarzania drzew. Lenistwo pozwala pracować z nieskończonymi kolekcjami.
Jeśli podoba Ci się JVM, polecam spojrzeć na Scalę i Clojure. Oba są wnikliwymi interpretacjami programowania funkcjonalnego. Scala jest bezpieczna dla typu z składnią nieco podobną do C, chociaż tak naprawdę ma tak wiele składni wspólnych z Haskellem jak z C. Clojure nie jest bezpieczny dla typu i jest Lispem. Niedawno opublikowałem porównanie Java, Scala i Clojure w odniesieniu do jednego konkretnego problemu z refaktoryzacją. Porównanie Logana Campbella z użyciem Game of Life obejmuje również Haskella i napisane Clojure.
PS
Jimmy Hoffa zwrócił uwagę, że moja klasa autobusów jest zmienna. Zamiast naprawić oryginał, myślę, że pokaże to dokładnie rodzaj refaktoryzacji tego pytania. Można to naprawić, ustawiając każdą metodę w autobusie jako fabrykę produkującą nowy autobus, każdą metodę w pasażerze - fabrykę produkującą nowego pasażera. Dlatego dodałem typ zwracany do wszystkiego, co oznacza, że skopiuję java.util.function.Function Java 8 zamiast interfejsu konsumenta:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Następnie w autobusie:
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));
}
Wreszcie anonimowy obiekt funkcji zwraca zmodyfikowany stan rzeczy (nowy autobus z nowymi pasażerami). Zakłada się, że p.debit () zwraca teraz nowego niezmiennego Pasażera z mniejszą ilością pieniędzy niż oryginał:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
Mamy nadzieję, że możesz teraz podjąć własną decyzję o tym, jak funkcjonalny chcesz uczynić swój imperatywny język, i zdecydować, czy lepiej byłoby przeprojektować swój projekt za pomocą funkcjonalnego języka. W Scali lub Clojure kolekcje i inne interfejsy API zostały zaprojektowane w celu ułatwienia programowania funkcjonalnego. Oba mają bardzo dobre współdziałanie Java, dzięki czemu można mieszać i dopasowywać języki. W rzeczywistości, dla interoperacyjności Java, Scala kompiluje swoje funkcje pierwszej klasy do anonimowych klas, które są prawie kompatybilne z interfejsami funkcjonalnymi Java 8. Możesz przeczytać o szczegółach w Scala w sekcie Głębokość. 1.3.2 .