Mam trudności ze znalezieniem zasobów na temat pisania programów w funkcjonalnym stylu. Najbardziej zaawansowanym tematem, jaki mogłem znaleźć w Internecie, było użycie pisania strukturalnego w celu ograniczenia hierarchii klas; większość po prostu zajmuje się tym, jak używać map / fold / zmniejsz / etc, aby zastąpić imperatywne pętle.
Naprawdę chciałbym znaleźć dogłębną dyskusję na temat implementacji OOP nietrywialnego programu, jego ograniczeń i sposobu refaktoryzacji w funkcjonalnym stylu. Nie tylko algorytm lub struktura danych, ale coś z kilkoma różnymi rolami i aspektami - być może gra wideo. Nawiasem mówiąc, przeczytałam Tomas Petricek, „Real-World Functional Programming”, ale chciałem więcej.
Odpowiedzi:
Definicja programowania funkcjonalnego
Wstęp do Radości Clojure mówi:
Programowanie w Scali Wydanie 2 s. 10 ma następującą definicję:
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:
Funkcjonalny (przy użyciu anonimowej funkcji lub „lambda” w Scali):
Bardziej słodka wersja Scali:
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:
Następnie klasa Bus zapewnia wewnętrzny iterator:
Na koniec przekazujesz anonimowy obiekt funkcji do magistrali:
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:
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:
Następnie w autobusie:
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ł:
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 .
źródło
Mam osobiste doświadczenie w „osiągnięciu” tego. W końcu nie wymyśliłem czegoś, co jest czysto funkcjonalne, ale wymyśliłem coś, z czego jestem zadowolony. Oto jak to zrobiłem:
x
metoda obiektu ulegnie modyfikacji , zmień ją tak, aby metoda była przekazywanax
zamiast wywoływaniathis.x
.x.methodThatModifiesTheFooVar()
nafooFn(x.foo)
map
,reduce
,filter
, itd.Nie mogłem pozbyć się stanu zmiennego. W moim języku (JavaScript) było to zbyt mało idiomatyczne. Ale poprzez przekazanie i zwrócenie wszystkich stanów, każdą funkcję można przetestować. Różni się to od OOP, gdzie konfiguracja stanu zajęłaby zbyt długo lub oddzielenie zależności często wymaga najpierw modyfikacji kodu produkcyjnego.
Mogę się również mylić co do definicji, ale myślę, że moje funkcje są referencyjnie przejrzyste: moje funkcje będą miały taki sam efekt przy tych samych danych wejściowych.
Edytować
Jak widać tutaj , w JavaScript nie można stworzyć naprawdę niezmiennego obiektu. Jeśli jesteś sumienny i kontrolujesz, kto wywołuje Twój kod, możesz to zrobić, zawsze tworząc nowy obiekt zamiast mutować bieżący. Nie było to dla mnie warte wysiłku.
Ale jeśli używasz Javy , możesz użyć tych technik, aby uczynić swoje klasy niezmiennymi.
źródło
Nie sądzę, że naprawdę można całkowicie zrefaktoryzować program - musiałbyś przeprojektować i ponownie wdrożyć w prawidłowym paradygmacie.
Widziałem refaktoryzację kodu zdefiniowaną jako „zdyscyplinowana technika restrukturyzacji istniejącego fragmentu kodu, zmieniająca jego wewnętrzną strukturę bez zmiany jego zachowania zewnętrznego”.
Możesz sprawić, by pewne rzeczy były bardziej funkcjonalne, ale nadal masz program zorientowany obiektowo. Nie możesz po prostu zmieniać małych kawałków, aby dostosować go do innego paradygmatu.
źródło
Myślę, że ta seria artykułów jest dokładnie tym, czego chcesz:
Czysto funkcjonalne Retrogames
http://prog21.dadgum.com/23.html Część 1
http://prog21.dadgum.com/24.html Część 2
http://prog21.dadgum.com/25.html Część 3
http://prog21.dadgum.com/26.html Część 4
http://prog21.dadgum.com/37.html Dalsze działania
Podsumowanie to:
Autor sugeruje główną pętlę z efektami ubocznymi (efekty uboczne muszą się gdzieś zdarzyć, prawda?), A większość funkcji zwraca małe niezmienne zapisy opisujące, w jaki sposób zmieniły stan gry.
Oczywiście, pisząc program z prawdziwego świata, będziesz mieszał i dopasowywał kilka stylów programowania, wykorzystując każdy, w którym najbardziej pomaga. Jednak dobrym doświadczeniem w nauce jest pisanie programu w najbardziej funkcjonalny / niezmienny sposób, a także pisanie go w najbardziej spaghetti, używając tylko zmiennych globalnych :-) (proszę zrobić to w ramach eksperymentu, a nie w produkcji)
źródło
Prawdopodobnie musiałbyś wywrócić cały kod na lewą stronę, ponieważ OOP i FP mają dwa przeciwne podejścia do organizowania kodu.
OOP organizuje kod wokół typów (klas): różne klasy mogą implementować tę samą operację (metoda o tej samej sygnaturze). W rezultacie OOP jest bardziej odpowiednie, gdy zestaw operacji niewiele się zmienia, a nowe typy można bardzo często dodawać. Rozważmy na przykład bibliotekę GUI, w którym każdy widget ma stały zestaw metod (
hide()
,show()
,paint()
,move()
, i tak dalej), ale nowe widżety mogą być dodawane jako biblioteki zostanie przedłużony. W OOP łatwo jest dodać nowy typ (dla danego interfejsu): wystarczy dodać nową klasę i zaimplementować wszystkie jej metody (lokalna zmiana kodu). Z drugiej strony dodanie nowej operacji (metody) do interfejsu może wymagać zmiany wszystkich klas implementujących ten interfejs (nawet jeśli dziedziczenie może zmniejszyć ilość pracy).FP organizuje kod wokół operacji (funkcji): każda funkcja implementuje pewne operacje, które mogą traktować różne typy na różne sposoby. Zwykle osiąga się to poprzez wysyłkę do typu poprzez dopasowanie wzoru lub inny mechanizm. W rezultacie FP jest bardziej odpowiedni, gdy zestaw typów jest stabilny, a nowe operacje są dodawane częściej. Weźmy na przykład stały zestaw formatów obrazu (GIF, JPEG itp.) I niektóre algorytmy, które chcesz wdrożyć. Każdy algorytm może być zaimplementowany przez funkcję, która zachowuje się inaczej w zależności od typu obrazu. Dodanie nowego algorytmu jest łatwe, ponieważ wystarczy zaimplementować nową funkcję (zmiana kodu lokalnego). Dodanie nowego formatu (typu) wymaga modyfikacji wszystkich funkcji, które do tej pory zaimplementowałeś w celu jego obsługi (zmiana nielokalna).
Konkluzja: OOP i FP różnią się zasadniczo pod względem sposobu organizacji kodu, a zmiana projektu OOP na projekt FP wymagałaby zmiany całego kodu w celu odzwierciedlenia tego. Może to być ciekawe ćwiczenie. Zobacz także te notatki do wykładu do książki SICP cytowanej przez mikemay, w szczególności slajdy od 13.1.5 do 13.1.10.
źródło