Przejdź do głównej zawartości

Pragmatyczne Programowanie Funkcyjne


Przechodzenie do programowania funkcyjnego rozpoczęło się na dobre jakąś dekadę temu. Widzieliśmy jak języki Scala, Clojure i F# zaczęły przyciągać uwagę. To przechodzenie było czymś więcej niż tylko zwykłym entuzjazmem w stylu: "O fajnie, nowy język!". Było w tym coś prawdziwego. Coś, co to napędzało - przynajmniej tak myśleliśmy.

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

Prawo Moora mówiło, że prędkość komputerów będzie podwajać się każdorazowo co 18 miesięcy. To prawo sprawdzało się od lat 60-tych aż do roku 2000. I wtedy się zatrzymało. Na amen. Częstotliwości zegara osiągnęły 3ghz i krzywa wzrostu spłaszczyła się. Prędkość światła została osiągnięta. Sygnały nie mogły rozchodzić się po układzie elektronicznym z wystarczającą prędkością, aby zapewnić szybsze działanie.
Więc projektanci sprzętu zmienili swój plan działania. Aby zapewnić większą wydajność, dodali więcej procesorów (rdzeni). Żeby zrobić miejsce dla tych rdzeni usunęli wiele elementów pamięci podręcznej i potokowości z tych układów. Zatem procesory stały się odrobinę wolniejsze niż przedtem, ale za to było ich więcej. Wydajność zwiększyła się.
Miałem swoją pierwszą dwurdzeniową maszynę 8 lat temu. Dwa lata później miałem czterordzeniową maszynę. I tak zaczęło się rozmnażanie rdzeni. I wszyscy zrozumieliśmy, że to wpłynie na tworzenie oprogramowania w sposób, którego nie mogliśmy przewidzieć.
Jedną z naszych reakcji na to wszystko było uczenie się Programowania Funkcyjnego (PF). PF mocno zniechęca do zmiany stanu zmiennej raz zainicjalizowanej. Ma to zasadniczy wpływ na współbieżność. Jeżeli nie możesz zmienić stanu zmiennej, nie masz problemu sytuacji wyścigu. Jeżeli nie możesz zaktualizować wartości zmiennej, nie masz problemu jej jednoczesnego nadpisania.
Uważano to za rozwiązanie problemu wielu rdzeni. W momencie, gdy rdzenie rozmnażały się, współbieżność, BA! jednoczesność stała się znaczącym problemem. PF miało więc zapewnić styl programowania, który łagodziłby problemy związane z obsługą 1024 rdzeni w procesorze.
Więc wszyscy zaczęli uczyć się Clojure, lub Scali, lub F# lub Haskella; ponieważ widzieli, że pociąg towarowy już pędził po torach i zmierzał w ich kierunku, a oni chcieli być przygotowani, kiedy nadjedzie.
Ale pociąg towarowy nigdy nie nadjechał. Sześć lat temu miałem laptopa z czterema rdzeniami. On tego czasu miałem ich jeszcze dwa. Wygląda na to, że mój następny laptop też będzie miał cztery rdzenie. Czy jesteśmy świadkami kolejnego spłaszczenia krzywej wzrostu?
Tak na marginesie, wczoraj wieczorem oglądałem film z 2007 roku. Bohaterka używała laptopa, przeglądała strony używając wymyślnej przeglądarki, używała Google'a i odbierała SMSy na telefonie z klapką. To wszystko wyglądało bardzo znajomo. Ooo, jasne, że minęło już trochę czasu - widziałem starszy model laptopa, starszą wersję przeglądarki i telefon z klapką nie przypominał dzisiejszych smartfonów. Nadal - zmiana ta nie była aż tak dramatyczna jak od roku 2000 do roku 2011. I nawet nie zbliżyła się do tej zmiany, jaka miała miejsce pomiędzy latami 1990 - 2000. Czy jesteśmy świadkami kolejnego spłaszczenia krzywej wzrostu w dziedzinie komputerów i technologii oprogramowania?
Więc, być może PF nie było tak kluczową umiejętnością, jak wtedy myśleliśmy. Może nie utoniemy pod zalewem rdzeni. Może nie musimy się martwić układami zawierającymi 32768 rdzeni. Może powinniśmy się odprężyć i wrócić do aktualizowania wartości zmiennych.
Myślę, że to byłby błąd. Duży błąd. Myślę, że to byłby błąd tak duży, jak niepowstrzymane użycie goto. Myślę, że byłoby to tak niebezpieczne, jak porzucenie dynamicznych, polimorficznych wywołań funkcji.
Dlaczego? Możemy zacząć argumentować od tego, co nas najbardziej interesuje. PF sprawia, że współbieżność jest bezpieczniejsza. Jeżeli budujesz system z wieloma wątkami, lub procesami, wtedy użycie PF znacząco zmniejszy problemy, które mógłbyś mieć z sytuacjami wyścigu i jednoczesną aktualizacją zmiennych.
Co jeszcze? Cóż, PF jest łatwiejsze do pisania, łatwiejsze do czytania, łatwiejsze do testowania i łatwiejsze do zrozumienia. Wyobrażam sobie, jak teraz część z was macha rękami i krzyczy do monitora. Spróbowałeś PF i stwierdziłeś, że nie znajdujesz w tym niczego łatwego. Wszystkie te operacje map i reduce, i cała ta rekurencja - szczególnie rekurencja ogonowa to nic prostego. Jasne. Rozumiem. Ale to tylko problem z zaznajomieniem się. Jak tylko obeznasz się z tymi pomysłami - aby rozwinąć taki stopień zaznajomienia nie trzeba dużo czasu - programowanie stanie się o wiele prostsze.
Dlaczego staje się prostsze? Ponieważ nie musisz śledzić stanu systemu. Stan zmiennych nie może się zmieniać; więc stan systemu pozostaje niezmieniony. I to nie tylko systemu nie musisz śledzić. Nie musisz śledzić żadnego stanu listy, czy stanu zbioru, czy stanu stosu, czy kolejki; bo te struktury danych nie moga być zmienione. Jeżeli wkładasz element na stos w języku PF, dostajesz nowy stos, nie zmieniając starego. To oznacza, że programista może żonglować większą ilością piłeczek w tym samym czasie. Jest mniej do zapamiętania. Mniej do śledzenia. I w ten sposób kod jest prostszy do napisania, czytania, rozumienia i testowania.
Więc jakiego języka powinieneś używać? Moim ulubionym jest Clojure. Powód jest taki, że Clojure jest absolutnie prosty. To jest dialekt Lisp, który jest wspaniale prostym językiem. Proszę bardzo, pozwól, że Ci zademonstruję.
To jest funkcja w Javie: f(x);
Teraz, aby zmienić ja w funkcję w Lispie, musisz po prostu przesunąć pierwszy nawias w lewo: (f x).
Teraz znasz już 95% Lispa, i umiesz około 90% Clojure. Ta śmieszna, mała składnia dotyczy większości tego rodzaju języków. To jest absurdalnie proste.


OK, może już widziałeś kiedyś programy w Lispie i nie spodobały Ci się te wszystkie nawiasy. I może nie lubisz tych operacji CAR i CDR i CADR i innych. Nie martw się. Clojure ma trochę lepszą interpunkcję niż Lisp, więc będzie trochę mniej nawiasów. Clojure zamieniło CAR i CDR i CADR na first i rest i second. Co więcej, Clojure jest zbudowany na JVM, co z kolei umożliwia kompletny dostęp do pełnej biblioteki Javy. Interoperacyjność jest szybka i łatwa. I, jeszcze lepiej, Clojure pozwala na pełny dostęp do możliwości OO JVM-a.
"Ale zaraz, zaraz!". Słyszę, jak mówisz. "PF i OO się wzajemnie wykluczają!". Kto Ci to powiedział? To bzdury! Ooo, to prawda, że w PF nie możesz zmienić stanu obiektu; ale co z tego? Tak jak wrzucenie liczby całkowitej na stos daje w wyniku nowy stos - wtedy, kiedy wywołujesz metodę, która aktualizuje jakąś wartość z obiektu, dostajesz nowy obiekt, zamiast zmieniać stary. Łatwo to ogarnąć, szczególnie jak przywykniesz.
Ale wracając do OO. Jedną z własności OO, którą uważam za najbardziej przydatną, w kontekście architektury oprogramowania, jest dynamiczny polimorfizm. I Clojure dostarcza całkowity dostęp do dynamicznego polimorfizmu Javy. Być może przykład wyjaśni to najlepiej.
(defprotocol Gateway
  (get-internal-episodes [this])
  (get-public-episodes [this]))
Powyższy kod definiuje polimorficzny interfejs dla JVM. W Javie ten interfejs wyglądałby tak:
public interface Gateway {
  List<Episode> getInternalEpisodes();
  List<Episode> getPublicEpisodes();
}
Na poziomie JVM wyprodukowany bajtkod jest identyczny. Dzięki tej samej cesze program w Clojure może implementować interfejs Javowy. W Clojure wygląda to tak:
(deftype Gateway-imp [db]
  Gateway
  (get-internal-episodes [this]
    (internal-episodes db))
  (get-public-episodes [this]
    (public-episodes db)))
Zauważ ten argument konstruktora db, i jak wszystkie metody mają dostęp do niego. W tym przypadku implementacje interfejsu po prostu przekazują go do funkcji lokalnych.
Najlepszy z tego wszystkiego, być może, jest fakt, że Lisp, a co za tym idzie Clojure, jest (uwaga) Homoikoniczny, co oznacza, że kod jest danymi, na których program operuje. To łatwo zobaczyć. Ten kod: (1 2 3) reprezentuje listę trzech liczb całkowitych. Jeżeli pierwszy element listy będzie funkcją, tak jak w: (f 2 3) stanie się to wywołaniem funkcji. Z tego, wszystkie wywołania funkcji w Clojure są listami; a listy mogą być bezpośrednio manipulowane przez kod. Z tego wynika, że program potrafi stworzyć i uruchomić inne programy.
Podsumowanie jest takie. Programowanie funkcyjne jest ważne. Powinieneś uczyć się go. I jeśli zastanawiasz się jakiego języka mógłbyś się uczyć, ja sugeruję Clojure.

Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

Komentarze

Popularne posty z tego bloga

Kursy IT na Pluralsight. Dlaczego warto?

Bardzo sobie cenię kursy na Pluralsight. Mam wrażenie, że każdy kurs, który przeszedłem na tej platformie, w dużym stopniu podniósł moje zdolności. Wiem, dostęp do tej platformy nie jest tani, ale w mojej ocenie warty swojej ceny. To nie jest reklama, ale forma entuzjazmu jaki mam do tej formy samodoskonalenia. O to kilka punktów pokazujących ofertę tego serwisu i dlaczego warto skorzystać: Pluralsight to kursy z Javascript, C#, Java, Angular, Python, MySQL i wielu innych technologii i umiejętności. Kursy na Pluralsight w większości mają wyższą jakość niż te, które możemy znaleźć na przykład na YouTube. Są wyselekcjonowane, mają wysoką jakość dźwięku i obrazu. Często wgryzają się głęboko w dany problem daleko poza standardowe „Hello World” danej technologii. Twórcy Pluralsight to często osoby znane ze świata IT i konferencji branżowych, jak: Scott Hanselman, Microsoft John Somnez, SimpleProgrammer.com John Skeet, Google Pluralsight udostępnia funkcjonalność ścieżek – paths.

Algorytm Dijkstry

Byłem jednego dnia na SCNA , i ktoś zagadnął mnie o TDD i algorytm Dijkstry . Zastanawiał się, czy można znaleźć sekwencję testów, która zaprowadzi do tego algorytmu. To wyglądało mi na fajne, krótkie ćwiczenie, więc zdecydowałem się spróbować. Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony: https://blog.cleancoder.com/uncle-bob/2016/10/26/DijkstrasAlg.html Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta. Zacząłem tak, jak zwykle; przez odpalenie ograniczonego przypadku testowego. public class MinPathTest { @Test public void nothing() throws Exception { } } Algorytm Dijkstry jest prostym sposobem znajdowania najkrótszej drogi w grafie o krawędziach mających konkretną długość. Podając węzeł startowy i końcowy, algorytm wskaże, jaka jest najkrótsza ścieżka i jaka jest jej długość. A więc już od samego początku są ciekawe decyzje do podjęcia. W jaki sposób p

Podstawy Programowania Funkcyjnego Epizod 3

Czy wszystkie Zasady Się Zmieniają? Kiedy tylko zaczynamy używać nowego paradygmatu , porównujemy z nim nasze dotychczasowe zasady i nawyki. Pytamy siebie czy te wszystkie zasady i nawyki są poprawne w kontekście tego nowego paradygmatu. Rozważ, dla przykładu: Test Driven Development . Czy nadal jest poprawne w Programowaniu Funkcyjnym? Jeżeli tak, to jak się do tego zabierzesz? Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 07 stycznia 2013 ze strony: https://blog.cleancoder.com/uncle-bob/2013/01/07/FPBE3-Do-the-rules-change.html Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta. Aby odpowiedzieć sobie na to pytanie spróbujmy napisać prosty funkcyjny program: Word Wrap (zawijanie tekstu). Wymagania są proste. Mając napis złożony ze słów, oddzielonych pojedynczymi spacjami