Dane oryginału

Original edition copyright © 2017 by The Pragmatic Programmers, LLC. All rights reserved. Title of English-language original: Functional Programming: A PragPub Antology by Michael Swaine and the PragPub writers, ISBN 978-1-68050-233-6. Polish-language edition © 2020 by Polish Scientifi c Publishers PWN Wydawnictwo Naukowe PWN Spółka Akcyjna. All Rights reserved.

Przekład Małgorzata Dąbkowska-Kowalik i Witold Sikorski

na zlecenie WITKOM Witold Sikorski

Projekt okładki polskiego wydania Joanna Andryjowicz

Wydawca: Wioleta Szczygielska-Dybciak

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Irena Puchalska

Koordynator produkcji Anna Bączkowska

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN

Weronika Panecka / Woblink

Książka, którą nabyłeś, jest dziełem twórcy i wydawcy. Prosimy, abyś przestrzegał praw, jakie im przysługują. Jej zawartość możesz udostępnić nieodpłatnie osobom bliskim lub osobiście znanym. Ale nie publikuj jej w internecie. Jeśli cytujesz jej fragmenty, nie zmieniaj ich treści i koniecznie zaznacz, czyje to dzieło. A kopiując jej część, rób to jedynie na użytek osobisty.

Szanujmy cudzą własność i prawo.

Więcej na www.legalnakultura.pl

Polska Izba Książki

Copyright © for the Polish edition by Wydawnictwo Naukowe PWN SA

Warszawa 2020

ISBN 978-83-01-21046-5

eBook został przygotowany na podstawie wydania papierowego z 2020 r., (wyd. I)

Warszawa 2020

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl; reklama@pwn.pl

www.pwn.pl

Spis treści

Wprowadzenie

Podziękowania

Część I

Paradygmat funkcyjny

1. Programowanie funkcyjne wraca do łask

Widzieliśmy już ten film

Nowe argumenty za programowaniem funkcyjnym

2. Myślenie funkcyjne dla imperatywnego umysłu

Wszystko sprowadza się do funkcji

Chodzi o niemutowalność

Chodzi o sposób myślenia

Część II

Scala – język hybrydowy

3. Scala i styl funkcyjny

Czystość funkcyjna

Funkcje wyższego rzędu

Prosty przykład

Przykład praktyczny

Co ze znajdowaniem błędów i wydajnością?

4. Praca z kolekcjami języka Scala

Kolekcje niemutowalne

Kolekcje zmienne (mutowalne)

Kolekcje leniwe

5.Tworzenie funkcji wyższego rzędu w języku Scala

Tworzenie funkcji wyższego rzędu

Wiele list parametrów

Wartości funkcji i wzorzec pożyczki

Część III

Clojure – nowy Lisp

6. Wywiad z Richem Hickeyem

Dlaczego Clojure?

Infrastruktura

Z czym go porównać?

7.Zrozumienie języka Clojure – dlaczego Lisp nadal się liczy

REPL

Wektory i słowa kluczowe

Makra

8. Tożsamość, wartość i stan w Clojure

Model obiektowy

Model Clojure

9. Programowanie współbieżne w Clojure

Prosty problem programowania współbieżnego

Rozwiązania w Clojure

Część IV

Elixir – aby programowanie znów było przyjemnością

10. Wzorce i przekształcenia w języku Elixir

Dopasowywanie do wzorców

Dopasowywanie do wzorców dla danych strukturalnych

Dopasowywanie do wzorców i funkcje

Przekształcenie to zadanie numer 1

11. Stawanie się funkcyjnym za pomocą Elixira

Funkcje anonimowe

Funkcje nazwane

Przykład praktyczny

Refaktoryzacja do stylu funkcyjnego

Co wyróżnia ten kod

12. Równoległość w języku Elixir

Model aktora

Aktory a Elixir

Komunikaty

Monitorowanie naszego procesu

Ostatni przykład

Współbieżność to istota Elixira

Część V

Haskell – plac ćwiczeń dla uczonych

13. Haskell i myślenie funkcyjne

O co w tym wszystkim chodzi

Szybkie ćwiczenie

Typy danych są niedrogie

Dopasowywanie do wzorców

Rekurencja, sterowanie i funkcje wyższego rzędu

Inne własności

14. Haskell w praktyce

Po jednym kroku

Generowanie kandydatów

Filtrowanie słownikowe

Wyszukiwanie wszerz

Użycie wyszukiwania

Wydajność i optymalizacja

Część VI

Swift – programowanie funkcyjne dla aplikacji mobilnych

15. Swift – co powinniśmy wiedzieć

Hello, Swift!

Funkcyjny Swift

16. Myślenie funkcyjne w języku Swift

Nie używamy nil, chyba że celowo

Unikanie stanu mutowalnego

Należy używać funkcji wyższego rzędu

Część VII

Idziemy głębiej

17. Protokoły – Swift kontra Ruby i Elixir

Problem z rozszerzeniami

Przypadek dla protokołów

Protokoły i rozszerzenia

18. Dopasowywanie do wzorca w Scali

Liczenie monet

Dopasowywanie wszystkich rzeczy

Korzystanie z wyodrębniania

19. Współbieżność w Scali

Korzystanie z kolekcji równoległych

Wiedza, kiedy użyć współbieżności

Powrót do wcześniejszego przykładu

20. Wyjątkowa obsługa wyjątków w Clojure

Prosty przykład

Problem z wyjątkami

Rozwiązanie – warunki

Ułatwmy życie elementom wywołującym

Lenistwo i błędy

21. Testowanie platformy dla Elixira

Inwestowanie w testowanie

Jeden eksperyment, kilka miar

Optymalizowanie konfiguracji za pomocą TrueStory

Zagęszczanie i łączenie miar

Kontrolowanie powtarzania konfiguracji z zagnieżdżonymi kontekstami

Kontrolowanie powtarzania konfiguracji za pomocą potoków historyjek

22. Tworzenie danych testowych w języku Elixir

Typowe podejścia

Piękne dane dla pięknych testów

Rejestrowanie szablonów i prototypów za pomocą Forge

Tworzenie instancji wpisów szablonów

Wzajemne atrybuty i having

Tworzenie struktur

Tworzenie niestandardowych jednostek

Przystosowywanie zapisywania

23. System typów w języku Haskell

TL;DR (Too long; didn't read– zbyt długie, nie dało się odczytać)

Do czego służą typy?

Konkretny przykład – sortowanie

Język systemu typów Haskella

Wnioskowanie i sprawdzanie typów

Kilka przykładów

Wygodna przerwa

Interfejsy i klasy typów

Rzeczywiste przykłady z interfejsami

Zalety i wady – reguła 80/20

Po Haskellu – typy zależne

Twierdzenia są typami, a dowody programami

Inne spojrzenie na sortowanie

Wracamy na ziemię

24. Projekt w Haskellu – testowanie kodu natywnego

Nasz kod natywny

Krótkie wprowadzenie do FFI Haskella

Opakowywanie naszego natywnego kodu w Haskellu

Eksperymentowanie z GHCi

Krótkie wprowadzenie do QuickCheck

Pisanie własności równoważności

Likwidacja usterek

25. Wiele twarzy funkcji Swifta

Anatomia funkcji Swifta

Wywoływanie wszystkich funkcji

Wywoływanie metod

Metody instancji są funkcjami rozwiniętymi

Init – uwaga specjalna

Wyszukane parametry

Kontrola dostępu

Wyszukane typy zwrotne

Funkcje zagnieżdżone

26. Funkcyjne podejście do Lua

Funkcje pierwszoklasowe w Lua

Rekurencja w Lua

Budowanie za pomocą pierwotnych elementów funkcyjnych

Prosta animacja w grze

O autorach

Bibliografia

Przypisy

Wprowadzenie

Michael Swaine

W tej książce pokazano podejście do paradygmatu programowania funkcyjnego w pięciu różnych językach. Kolejne rozdziały zostały napisane przez ekspertów w zakresie tych języków pojawiły się pierwotnie jako artykuły w czasopiśmie PragPub. Po opublikowaniu niemal stu wydań czasopisma stało się jasne, że dysponujemy bogactwem informacji o programowaniu funkcyjnym i zdecydowaliśmy, że najlepsze artykuły złożą się na ciekawą książkę.

Programowanie funkcyjne jest jednym z głównych paradygmatów programowania. Podchodzimy w nim do obliczeń jako do wyznaczania wartości funkcji matematycznych, w miarę możliwości unikając zmiany stanu i mutowalnych danych.

W rozmowach o programowaniu funkcyjnym zawsze pojawiają się określone pojęcia i zagadnienia: rekurencja, leniwe obliczenia, przezroczystość odwołań, eliminowanie efektów ubocznych, funkcje jako obiekty pierwszej klasy, funkcje wyższego rzędu, rozwijanie (currying), dopasowywanie do wzorców. Autorzy poruszają wszystkie te tematy, spoglądając na nie z perspektywy różnych języków.

Ale programowanie funkcyjne to nie jest podejście typu „wszystko albo nic”. Całkiem sensowne jest napisanie kodu imperatywnego, który wykorzystuje niektóre techniki funkcyjne, praktyki i struktury danych. Do przyjęcia jest łączenie i dopasowywanie styli, a niektóre języki programowania są zaprojektowane jako hybrydy, pozwalając na używanie stylu, który najlepiej pasuje do naszych bieżących potrzeb – na przykład Scala, Mathematica lub Swift. Autorzy analizują w książce te różne podejścia, a my sami możemy zdecydować, co nam najbardziej odpowiada.

Książka zawiera analizy programowania funkcyjnego w pięciu językach, opisane przez ekspertów w zakresie danego języka. Jeśli zachęci to Czytelników do przeczytania pełnego przewodnika po niektórych językach, to polecamy książki: Venkat Subramaniam, Pragmatic Scala [Sub15], Stuart Halloway i Aaron Bedra, Programming Clojure (wyd. 2) [HB12], Dave Thomas, Programming Elixir 1.3 [Tho16], Chris Eidhof, Florian Kugler i Wouter Swiersta, Functional Swift (https://www.objc.io/books/functional-swift/), a także Miran Lipovaca, Learn You a Haskell for Great Good!: A Beginner’s Guide [Lip11].

W tej książce znajdziecie analizę ważnego paradygmatu programowania w pięciu językach napisaną przez zespół ekspertów. To chcieliśmy uzyskać.

Mam nadzieję, że zgodzicie się, że wyszła z tego ciekawa książka.

Podziękowania

Nie po raz pierwszy wydawca może z przyjemnością podziękować kolegom z grupy Pragmatic Programmers – profesjonalistom, z którymi przyjemnie się pracuje, i cudownym ludziom. Książka ta wiele zyskała dzięki umiejętności oceny wydawcy Andy Hunt, która rozumiała wizję książki; wiceprezes ds. działań Janet Furlow, która doprowadziła do jej zrealizowania; głównej redaktorki Susannah Davidson Pfalze, która pomogła nadać jej kształt, oraz redaktorce Nicole Abramowitz, która poprawiła niedociągnięcia.

Większość rozdziałów tej książki pojawiła się najpierw jako artykuły w czasopiśmie PragPub i jestem wdzięczny autorom nie tylko za oryginalne artykuły, lecz także za redakcję – w niektórych przypadkach znaczną – która pomogła przekształcić artykuły w rozdziały książki. Przeczytajcie tekst O autorach. Myślę, że będziecie pod wrażeniem.

Muszę też podziękować firmie Gilson Graphics za pracę przy projekcie i produkcji. Nancy Groth dziękuję za redakcję oryginalnych artykułów, a redaktorom technicznym – Paulowi Butcherowi, Ianowi Dees, Jeffowi Heonowi, Ronowi Jeffriesowi i Kim Shrier – za poświęcenie czasu i wiedzy.

Mam nadzieję, że spodoba wam się ta książka, która ma zapoczątkować serię antologii PragPub.

Część I

Paradygmat funkcyjny

Dlaczego programowanie funkcyjne ma znaczenie i jak zacząć myśleć w sposób funkcyjny.

ROZDZIAŁ 1

Programowanie funkcyjne wraca do łask

Michael Swaine

Lato, gdy przeniosłem się do Doliny Krzemowej, było tym latem, w którym zlikwidowano ILLIAC IV w Moffett Field.

Widzieliśmy już ten film

W 1981 roku centrum zainteresowania informatyki przeniosło się z ogromnych maszyn obsługiwanych przez kapłanów w białych fartuchach na tanie komputery osobiste tworzone i programowane przez niechlujnych hakerów. Ja przeniosłem się ze środkowego zachodu do Palo Alto i zgłosiłem się do pomocy w utworzeniu nowego tygodnika obsługującego tych niechlujów. W międzyczasie, w niewielkiej odległości – w ośrodku NASA Moffett Field oficjalnie zamknięto i rozłożono na części komputer, który zainspirował Stanleya Kubricka i Arthura C. Clarke’a do wymyślenia komputera HAL 9000.

ILLIAC IV to legendarny punkt zwrotny w projektowaniu komputerów, mający podobną pozycję w długiej i skomplikowanej historii programowania funkcyjnego.

Koncepcją leżącą u podstaw ILLIAC IV było oderwanie się od modelu sekwencyjnego, który od początku dominował w informatyce. Pewne obszary problemów, takie jak mechanika płynów, lepiej nadawały się do przetwarzania równoległego, a ILLIAC IV został specjalnie zaprojektowany dla tego rodzaju równoległych problemów – takich, w których jedna instrukcja mogła zostać zastosowana równolegle do wielu zbiorów danych. Jest to znane pod skrótem SIMD (single instruction, multiple data – jedna instrukcja, wiele danych). Bardziej ogólny przypadek – MIMD (multiple instructions operating on multiple data sets – wiele instrukcji działających na wielu zbiorach danych) jest trudniejszy. Ale każdy rodzaj równoległego programowania wymaga nowych algorytmów i nowego podejścia. I stanowi zaproszenie do programowania funkcyjnego.

Programowanie funkcyjne, określane skrótem FP, zgodnie z Wikipedią „traktuje obliczenia jako wyznaczanie matematycznych wartości funkcyjnych i unika zmiany stanu i zmiany danych”. Może to posłużyć za podstawową definicję, ale my musimy się zagłębić dużo bardziej.

FP funkcjonuje od lat pięćdziesiątych XX wieku, gdy John McCarthy wymyślił język Lisp, ale dążenie do przetwarzania równoległego dało programowaniu funkcyjnemu nowy impet. Unikanie zmiany stanu było wtedy trudną do przełknięcia pigułką, ale pasowało do modelu SIMD. To doprowadziło do rozwoju nowych języków i nowych cech FP w istniejących językach. Fortran był wtedy językiem, w którym większość wykonywała obliczenia naukowe, więc naturalne było jego rozszerzanie. Programiści ILLIAC IV mogli pisać programy w IVTRAN, TRANQUIL lub CFD, zrównoleglonych wersjach FORTRAN-u. Powstała też zrównoleglona wersja języka ALGOL.

Dla odpowiednich typów problemów, które można rozwiązać za pomocą SIMD, ILLIAC IV był najszybszym komputerem na świecie i utrzymał ten tytuł do czasu jego wycofania z eksploatacji w 1981 roku. Był o rząd wielkości szybszy od każdego komputera w tamtych czasach i był doskonale dopasowany do swoich docelowych zastosowań i programowania funkcyjnego.

Ale era ta gwałtownie się skończyła. 7 września 1981 roku ILLIAC IV został na dobre zatrzymany. Można dziś zobaczyć jego fragment w muzeum historii komputerów w Mountain View, niedaleko miejscowości Moffett Field.

Dlaczego ta era dobiegła końca? Według Wikipedii: „Illiac IV należał do klasy komputerów równoległych, określanych jako SIMD. Prawo Moore’a prześcignęło specjalizowane podejście stosowane w SIMD ILLIAC, sprawiając, że podejście MIMD stało się preferowane dla niemal wszystkich obliczeń naukowych”.

Po drodze zyskał jeszcze jedno miejsce w historii – inspirację dla komputera HAL 9000 w filmie 2001: Odyseja kosmiczna. Arthur C. Clarke nie był neofitą w kwestii komputerów – rozmawiał z Turingiem w Bletchley Park i pilnie śledził postępy w dziedzinie mikrokomputerów. Clarke był zaintrygowany, gdy dowiedział się o działaniu ILLIAC IV na Uniwersytecie Stanu Illinois w Urbana-Champaign i uczcił ten kampus w swoim filmie jako miejsce narodzin HAL-a 9000.

Przejdźmy do roku 2000. Zastosowanie, które sprawiło, że FP stało się warte nauki, okazało się znowu przetwarzaniem równoległym, ale napędzanym pojawieniem się procesorów wielordzeniowych. „Twórcy chipów”, jak wtedy twierdziłem, „mówili w istocie, że wdrażanie prawa Moore’a to teraz domena oprogramowania. Oni skoncentrują się na wstawianiu coraz więcej rdzeni do kości, a programiści muszą tak przerobić swoje programy, aby wykorzystać możliwości przetwarzania równoległego w ich układach”.

Impet, jaki zyskało FP na początku XXI wieku, wynikał z chęci oderwania się od modelu sekwencyjnego, przy czym zaoferowane zostały różne podejścia. Osoby, które zainwestowały wiele w umiejętności i narzędzia w Javie, nie chciały odkładać na bok bibliotek kodów, umiejętności i narzędzi, na których się opierały, mogły używać języka Scala Martina Odersky’ego, ostatnio wprowadzonego na platformie Javy. Zaoferowano go też na platformie .NET, choć (niestety) zaniechano jego wsparcia w 2012 roku. Użytkownicy .NET powinni raczej przymierzać się do F#, utworzonego przez Dona Syme’a z Microsoft Research. Kto chce mieć czysto funkcyjny język, może skorzystać z Erlang, opracowanego dla wysoce równoległego programowania przez Joe Armstronga z Ericssona, z języka Haskell lub wielu innych możliwości.

Tym, co oferują wszystkie te języki, jest możliwość pracy w paradygmacie funkcyjnym. Dwie podstawowe cechy paradygmatu funkcyjnego to potraktowanie wszystkich obliczeń jako wyznaczania wartości funkcyjnych oraz unikanie zmiany stanu i mutowalnych danych. Ale są też inne wspólne cechy programowania funkcyjnego:

• funkcje pierwszoklasowe – funkcje mogą służyć jako argumenty i wyniki funkcji;

• rekurencja jako podstawowe narzędzie iteracji;

• szerokie stosowanie dopasowywania do wzorców;

• leniwe wartościowanie, co pozwala na tworzenie nieskończonych ciągów.

Programiści iteracyjni, którzy po raz pierwszy stykają się z FP, mogą jeszcze dodać – jest ono wolne i niejasne. W istocie żadna z tych opinii nie występuje zawsze, ale wymaga to zmiany sposobu myślenia o problemach i różnych algorytmach. Aby programy działały odpowiednio, muszą mieć lepsze wsparcie ze strony języka niż to, które było powszechne w pierwszej dekadzie XXI wieku. To się zmieni w kolejnym dziesięcioleciu, a wraz z tym zmienią się przyczyny, dla których FP przyciąga ludzi do siebie.

Nowe argumenty za programowaniem funkcyjnym

Po upływie dekady ponownie wróciła sprawa programowania funkcyjnego. Pojawiło się nowe wsparcie językowe i sprawa FP stała się szersza.

Chociaż równoległość była tradycyjnie siłą napędową programowania funkcyjnego, teraz gdy mówi się o FP, częściej odwołuje się do zdolności spojrzenia na problem na wyższym poziomie oraz o zaletach niemutowalności. Zamiast patrzeć na FP jak na dziwny sposób, który trzeba zastosować, aby poradzić sobie z równoległością, nowym argumentem jest bardziej naturalny sposób pisania, bliższy pojęciom i terminologii dla naszej dziedziny. Ludzie używają FP do aplikacji, które nie wymagają równoległości, aby programować bardziej efektywnie i jasno, działając bliżej sposobu myślenia o swoich problemach.

Mówi się, że zamiast konieczności przełożenia problemu na język programowania, adaptujemy język do problemu.

A jeśli mamy dostępnych wiele rdzeni i równoległość może być korzystna dla naszego kodu, to mamy równoległość za darmo. Neal Ford powiedział: „Ostatnie mądre innowacje w bibliotekach [Clojure] pozwoliły przepisać funkcję odwzorowania tak, aby automatycznie stawała się równoległa, co oznacza, że wszystkie działania związane z odwzorowaniami skorzystają z przyspieszenia wydajności bez interwencji dewelopera”.

Dobre dopasowanie FP do współbieżności przemawia do ludzi, którzy piszą aplikacje wieloprocesorowe, aplikacje o dużej dostępności, serwery WWW dla sieci społecznościowych i do wielu innych zastosowań. Wyższy poziom abstrakcji FP przemawia do osób, które szukają krótszego czasu pracy dewelopera lub lepiej zrozumiałego kodu. FP kładzie nacisk na niemutowalność, co silnie przemawia do każdego, kto jest zainteresowany niezawodnością.

Wzrost sprzedaży stanowi prawdziwą zmianę z argumentacji na rzecz programowania funkcyjnego z czasów ILLIAC IV. Dziś są nowe powody, aby spojrzeć na FP, a także lepsze wsparcie językowe FP i większy wybór podejść. Zaczyna się to od wyboru języka. Można wygodnie trzymać się znanych sobie języków i ich narzędzi, wprowadzając funkcyjność tam, gdzie jest dla nas użyteczna. Lub też można przejść do języka zbudowanego od podstaw do programowania funkcyjnego. W tej książce zobaczymy oba te podejścia.

To ekscytujący czas na poznawanie programowania funkcyjnego, a korzyści z niego wynikające są znaczne. Ale wymaga to jednak innego sposobu myślenia.

W następnym rozdziale Michael Bevilacqua-Linn zaprasza nas do myślenia o programowaniu w sposób funkcyjny.

ROZDZIAŁ 2

Myślenie funkcyjne dla imperatywnego umysłu

Michael Bevilacqua-Linn

Dokładne określenie, czym w istocie jest programowanie funkcyjne, może być trudne.

Nie jest to łatwe, gdyż termin ten, choć dobrze zdefiniowany teoretycznie, w praktyce wskazuje kilka różnych, choć powiązanych ze sobą idei. Jeśli porozmawiamy z hakerem Closure, zapewne dużo będzie mówił o makrach. Programista w Haskellu może mówić o monadach, a programista w Erlangu o aktorach.

Są to różne pojęcia. Makro dają programistom niezwykle silne możliwości metaprogramowania, monady pozwalają na bezpieczne modelowanie zmian stanu, aktory zaś zapewniają rozbudowany sposób realizacji programowania rozproszonego i współbieżnego.

Mimo to wszystkie te różne idee uważa się za cechy języków funkcyjnych. Mnogość pojęć może nieco utrudnić określenie, o co w tym wszystkim chodzi. A jednak podstawowa idea jest prosta.

Wszystko sprowadza się do funkcji

W swojej istocie programowanie funkcyjne dotyczy programowania z czystymi funkcjami, bez efektów ubocznych.

Oto czysta funkcja:

f(x) = <raise power="2">x</raise>

A więc ta jest czysta:

public int incrementCounter(int counter) {

return counter++;

}

a ta nie jest:

private int counter = 0;

public void incrementMutableCounter() {

counter++;

}

Pierwsze dwa przykłady zwiększają licznik, zwracając nową wartość całkowitą, która jest o jeden większa od przekazanej liczby całkowitej. Trzeci przykład robi to samo, ale zmienia stan, który może być wspólny dla wielu fragmentów programu.

Definicja – funkcja taka jak incrementCounter, która nie jest oparta na zmianie stanu, jest nazywana funkcją czystą. A ta jej czystość daje wiele korzyści. Jeśli na przykład mamy funkcję czystą, która wykonuje jakieś kosztowne obliczenia, możemy zoptymalizować nasz program, wywołując tę funkcję tylko raz i przechwytując wynik – czyli stosując technikę zwaną memoizacją.

Czyste funkcje ułatwiają też analizę programów. Program obiektowy to graf obiektów, gdzie każdy ma wiele mutowalnych stanów. Modyfikacja stanu jednego z obiektów może prowadzić do modyfikacji stanu innego, który może się znajdować w grafie w odległości wielu węzłów. W programie, który ma tylko czyste funkcje, takie działanie na odległość jest niemożliwe.

To jest uproszczony opis programowania funkcyjnego.

Chodzi o niemutowalność

Niestety, pełna czystość nie ma się najlepiej w świecie rzeczywistym. Czyste funkcje mogą dobrze modelować pewne dziedziny, ale do innych nie pasują. Kompilatory to czyste funkcje. Wyszukiwanie w Google nie jest czyste.

Praktyczne języki programowania funkcyjnego kładą nacisk na niemutowalność i czystość funkcyjną, ale muszą mieć też możliwość modelowania zmieniającego się świata, który nie zapewnia pełnej czystości funkcji. W języku Haskell, który jest zapewne najbardziej ścisłym językiem funkcyjnym, można modelować zmiany, wykorzystując monady, zachowując przy tym ścisłą czystość.

Inne języki funkcyjne mają inne techniki minimalizowania i kontroli zmiany stanu, które nie są tak ścisłe jak te z Haskela. Clojure na przykład używa systemu programowej pamięci transakcyjnej w połączeniu ze zbiorem typów referencyjnych oraz bardzo sprytnych niemutowalnych struktur danych, aby utrzymać wysoki poziom czystości, a jedocześnie pozwolić programistom na radzenie sobie w zmieniającym się świecie.

Chodzi o sposób myślenia

Tak więc w pierwszym przybliżeniu programowanie funkcyjne to programowanie z czystymi funkcjami i niemutowalnym stanem, w przeciwieństwie do programowania imperatywnego, które silnie zależy od mutowalności. Wokół tej niemutowalnej podstawy mamy zbiór technik językowych i cech, które zastępują mutowalne techniki imperatywne. Ich analiza da nam lepsze poczucie, czym jest myślenie i programowanie funkcyjne.

Popatrzmy na prosty przykład – filtrowanie listy tak, aby zawierała tylko liczby nieparzyste.

public List<Integer> filterOdds(List<Integer> list) {

List<Integer> filteredList = new ArrayList<Integer>();

for(Integer current : list) {

if(1 == current % 2) {

filteredList.add(current);

}

}

return filteredList;

}

To jest kod imperatywny. Iterujemy po liście i sprawdzamy dla każdego elementu, czy liczba jest nieparzysta, obliczając jej modulo. Moglibyśmy napisać jej zawartość nieco jaśniej, gdybyśmy wyciągnęli sprawdzenie do funkcji pomocniczej i nadali jej nazwę.

public List<Integer> filterOdds(List<Integer> list) {

List<Integer> filteredList = new ArrayList<Integer>();

for (Integer current : list) {

if (isOdd(current)) {

filteredList.add(current);

}

}

return filteredList;

}

private boolean isOdd(Integer integer) {

return 1 == integer % 2;

}

Co należy zrobić, jeśli chcemy teraz utworzyć funkcję, która pozwoli nam odfiltrować liczby parzyste zamiast nieparzystych? Jedyny fragment kodu, który trzeba zmienić, to wywołanie isEven zamiast isOdd.

public List<Integer> filterEvens(List<Integer> list) {

List<Integer> filteredList = new ArrayList<Integer>();

for (Integer current : list) {

if (isEven(current)) {

filteredList.add(current);

}

}

return filteredList;

}

private boolean isEven(Integer integer) {

return 0 == integer % 2;

}

To działa, ale popełniliśmy jeden z kardynalnych grzechów kodowania. Większość filterOutEvens została wycięta i wklejona z filterOutOdds. W istocie chcemy mieć filtr (filter), który może korzystać z dowolnego fragmentu kodu, aby wykonać swoje filtrowanie.

Zastanówmy się, jak możemy dokonać tego w języku Java. Zarówno isOdd, jak i isEven pobierają jeden argument i zwracają wartość logiczną (boolean). Zdefiniujmy interfejs, który obejmuje istotę tych obliczeń. Nazwiemy go Predicate, co stanowi matematyczną nazwę funkcji, która zwraca wartość typu boolean.

public interface Predicate {

public boolean evaluate(Integer argument);

}

Możemy teraz przepisać filterEvens i filterOdds tak, aby były bardziej ogólne.

public List<Integer> filter(List<Integer> list, Predicate predykat) {

List<Integer> filteredList = new ArrayList<Integer>();

for (Integer current : list) {

if (predicate.evaluate(current)) {

filteredList.add(current);

}

}

return filteredList;

}

Następnie definiujemy dwa nasze predykaty.

class isEven implements Predicate {

public boolean evaluate(Integer argument) {

return 0 == argument % 2;

}

}

class isOdd implements Predicate {

public boolean evaluate(Integer argument) {

return 1 == argument % 2;

}

}

Możemy teraz po prostu utworzyć instancję jednego z predykatów i przekazać ją do metody filter. Jeśli pojawi się nowy sposób filtrowania naszej listy – powiedzmy, że zechcemy zachować tylko liczby całkowite, które są idealnymi kwadratami – możemy zdefiniować PerfectSquare zamiast wycinania i wklejania całej funkcji filtrującej.

To, co właśnie zrobiliśmy z metodą filter i interfejsem Predicate, symuluje koncepcje ze świata funkcyjnego – funkcje wyższego rzędu. Są to takie funkcje, które można przekazywać do innych funkcji lub takie, które mogą wracać z innych funkcji. Zastanówmy się, jak zrobilibyśmy podobne filtrowanie w Clojure – nowoczesnym i funkcyjnym wariancie języka Lisp.

(filter odd? [0 1 2 3])

(filter even? [0 1 2 3])

To jest to! Pierwsze, co można zauważyć, to znacznie krótszy zapis niż w wersji Javy. Druga rzecz to zapewne fakt, że nawiasy nie znajdują się w swoim zwykłym położeniu. Clojure i inne wersje języka Lisp do wywoływania funkcji wykorzystują notację z prefiksem. Oznacza to, że funkcja, która jest wywoływana, znajduje się na pierwszym miejscu w nawiasach, a argumenty są umieszczane po niej.

Pomijając różnice składniowe, zauważmy, jak wersja z Clojure używa wszystkich wbudowanych funkcji i cech języka. Nie musimy definiować interfejsu Predicate. Funkcja odd? to funkcja, która pobiera liczbę i zwraca true, jeśli jest ona nieparzysta, natomiast even? robi to samo w odniesieniu do liczb parzystych. Możemy przekazać te funkcje bezpośrednio do funkcji filtrującej dzięki mocy funkcji wyższego rzędu.

To zmienia nasze rozwiązanie imperatywne, w którym pisaliśmy kod związany ze szczegółami związanymi z iteracją po liście, w postać bardzo deklaratywną. Pracujemy na wyższym poziomie abstrakcji, który prowadzi nas często do opisu, jakich wyników potrzebujemy, zamiast szczegółów ich otrzymywania.

Gdy więc mowa jest o programowaniu funkcyjnym, to zwykle chodzi o co najmniej dwie oddzielne, choć powiązane ze sobą sprawy. Po pierwsze, mówimy o programowaniu za pomocą czystych funkcji. Ponieważ jest to nieosiągalne marzenie przy większości rzeczywistych problemów, w praktyce funkcyjne języki programowania koncentrują się na łatwiejszym stosowaniu niemutowalności oraz na możliwości kontrolowania zmienności, gdy jest ona rzeczywiście niezbędna.

Po drugie, mówi się o stylu programowania, który wyrósł wokół podstawy funkcyjnej. Jak widzieliśmy, ten styl opiera się w znacznym stopniu na funkcjach wyższego rzędu i związanych z tym technikami. Techniki te często tworzą kod, który opiera się na wyższym poziomie abstrakcji, jaki widzieliśmy przy korzystaniu z funkcji filtrowania w powyższym przykładzie, a nie na jawnych iteracjach.

Te dwa aspekty programowania funkcyjnego dają znaczące korzyści. Wyjątkowy nacisk na niemutowalność sprawia, że programy są łatwiejsze do zrozumienia. Zachowanie funkcji można zrozumieć, czytając kod samej funkcji i nie martwiąc się o to, że jakiś fragment zmiennego stanu może zależeć od czegoś, co jest oddalone o setki lub tysiące wierszy kodu. Korzystanie z funkcji wyższego rzędu prowadzi często do kodu deklaratywnego, który jest prostszy i bardziej bezpośredni od odpowiednika imperatywnego.

Nie myli się więc ktoś, kto myśląc o programowaniu funkcyjnym, rozważa dwie rzeczy: preferowanie programowania z czystymi funkcjami i styl programowania obejmujący funkcje wyższego rzędu w kodzie deklaratywnym.

Kolejnych kilkanaście rozdziałów jest uporządkowanych według języków, a kilka z nich poświęcono programowaniu funkcyjnemu w tych językach. Można przeskoczyć bezpośrednio do języka, który nas najbardziej interesuje, lub czytać rozdziały po kolei. Ale najlepiej zacząć od następnego rozdziału, w którym Venkat Subramaniam pokaże nam jak Scala, hybrydowy język funkcyjny, wdraża omawiane wyżej problemy.