Dlaczego programiści zakochują się w programowaniu funkcyjnym
Programowanie funkcyjne istnieje już od ostatnich 60 lat, ale jak dotąd było to zjawisko niszowe. Chociaż firmy takie jak Google polegają na ich kluczowych pojęciach, dzisiejszy przeciętny programista wie o tym niewiele lub nic.
Ale... wkrótce się to zmieni. Nie tylko języki takie jak Java czy Python przyjmują coraz więcej koncepcji z programowania funkcyjnego. Starsze języki, takie jak Haskell, są już całkowicie funkcyjne.
Ujmując to w prostych słowach, programowanie funkcyjne polega na budowaniu funkcji dla niemutowalnych zmiennych. W przeciwieństwie do tego programowanie obiektowe polega na posiadaniu względnie stałego zestawu funkcji, a Twoim zadaniem jest przede wszystkim modyfikacja lub dodawanie nowych zmiennych.
Ze względu na swoją naturę, programowanie funkcyjne świetne sprawdza się dla bardziej wymagających zadań, takich jak analiza danych oraz uczenie maszynowe. Nie oznacza to, że powinieneś pożegnać się z programowaniem obiektowym i zamiast tego przejść całkowicie na programowanie funkcyjne. Warto jednak znać podstawowe zasady, aby w razie potrzeby móc je wykorzystać na swoją korzyść.
Chodzi o to, żeby nie dopuścić do pojawienia się efektów ubocznych
Aby zrozumieć programowanie funkcyjne, musimy najpierw zrozumieć funkcje. Może to zabrzmieć nudno, ale koniec końców jest to bardzo potrzebne. Czytaj więc dalej.
Funkcja, naiwnie rzecz ujmując, jest rzeczą, która przekształca pewne dane wejściowe w pewne dane wyjściowe. Tylko że to nie zawsze jest takie proste. Rozważmy tę funkcję w Pythonie:
def square(x):
return x*x
Ta funkcja jest jednocześnie głupia i prosta; przyjmuje jedną zmienną x
, przypuszczalnie int
, a może float
lub double
, i wyrzuca kwadrat tej zmiennej.
A teraz rozważmy kolejną:
global_list = []
def append_to_list(x):
global_list.append(x)
Na pierwszy rzut oka wygląda na to, że funkcja przyjmuje zmienną x
, dowolnego typu, i nic nie zwraca, ponieważ nie ma instrukcji return
. Ale czekaj!
Funkcja nie działałaby, gdyby global_list
nie została wcześniej zdefiniowana, a jej wyjściem jest ta sama lista, choć zmodyfikowana. Mimo że global_list
nigdy nie została zadeklarowana jako wejście, zmienia się ona, gdy używamy tej funkcji:
append_to_list(1)
append_to_list(2)
global_list
Zamiast pustej listy, zwraca ona [1,2]
. To pokazuje, że lista jest rzeczywiście wejściem funkcji, mimo że nie powiedzieliśmy o tym wprost. I to może być problem.
Nieuczciwość w zakresie funkcji
Te ukryte wejścia lub wyjścia, w innych przypadkach mają swoją oficjalną nazwę, czyli efekty uboczne. Podczas gdy my używaliśmy tylko prostego przykładu, w bardziej złożonych programach mogą one powodować prawdziwe trudności.
Zastanów się, jak przetestowałbyś append_to_list
: Zamiast po prostu przeczytać pierwszą linię i przetestować funkcję z dowolnym x
, musisz przeczytać całą definicję, zrozumieć, co robi, zdefiniować global_list
i przetestować ją w ten sposób. To, co w tym przykładzie jest proste, może szybko stać się uciążliwe, gdy mamy do czynienia z programami zawierającymi tysiące linii kodu.
Dobrą wiadomością jest to, że łatwo to naprawić: bycie uczciwym w kwestii tego, co funkcja przyjmuje jako dane wejściowe. Tak jest o wiele lepiej:
newlist = []
def append_to_list2(x, some_list):
some_list.append(x)
append_to_list2(1,newlist)
append_to_list2(2,newlist)
newlist
Właściwie nie zmieniliśmy dużo. Wyjściem jest nadal [1,2]
, a wszystko inne pozostaje bez zmian.
Zmieniliśmy jednak jedną rzecz: kod jest teraz wolny od efektów ubocznych. I to jest bardzo dobra wiadomość.
Kiedy teraz patrzysz na deklarację funkcji, wiesz dokładnie, co się dzieje. Dlatego, jeśli program nie zachowuje się zgodnie z oczekiwaniami, możesz łatwo przetestować każdą funkcję samodzielnie i wskazać, która z nich jest wadliwa.
Programowanie funkcyjne to pisanie czystych funkcji
Funkcja z jasno zadeklarowanymi wejściami i wyjściami to taka, która nie ma efektów ubocznych. A funkcja bez efektów ubocznych to czysta funkcja.
Najprostsza definicja programowania funkcyjnego to: pisanie programu tylko w czystych funkcjach. Czyste funkcje nigdy nie modyfikują zmiennych, a jedynie tworzą nowe jako wyjście. (Oszukałem trochę w powyższym przykładzie: idzie on wzdłuż linii programowania funkcyjnego, ale nadal używa global list
. Znajdziesz lepsze przykłady, ale tu chodziło o podstawową zasadę).
Co więcej, możesz oczekiwać określonego wyjścia od czystej funkcji z danym wejściem. W przeciwieństwie do tego, kiedy funkcja nieczysta może zależeć od pewnej zmiennej globalnej; tak więc te same zmienne wejściowe mogą prowadzić do różnych wyników, jeśli zmienna globalna jest inna. To ostatnie może sprawić, że debugowanie i utrzymanie kodu będzie o wiele trudniejsze.
Istnieje prosta zasada wykrywania efektów ubocznych. Ponieważ każda funkcja musi mieć jakieś wejście i wyjście, deklaracje funkcji, które nie mają żadnego wejścia i wyjścia, muszą być nieczyste. Są to pierwsze deklaracje, które możesz chcieć zmienić, jeśli działasz z programowaniem funkcyjnym.
Czym nie jest programowanie funkcyjne (i tylko tym)
Map i reduce
Pętle nie są częstym gościem w programowaniu funkcyjnym. Rozważmy takie pętle Pythona:
integers = [1,2,3,4,5,6]
odd_ints = []
squared_odds = []
total = 0
for i in integers:
if i%2 ==1
odd_ints.append(i)
for i in odd_ints:
squared_odds.append(i*i)
for i in squared_odds:
total += i
Dla prostych operacji, które próbujesz wykonać, ten kod jest dość długi. To również nie jest funkcyjne, ponieważ modyfikujesz zmienne globalne.
Zamiast tego, rozważ to:
from functools import reduce
integers = [1,2,3,4,5,6]
odd_ints = filter(lambda n: n % 2 == 1, integers)
squared_odds = map(lambda n: n * n, odd_ints)
total = reduce(lambda acc, n: acc + n, squared_odds)
To jest w pełni funkcyjne. Krótsze i szybsze, ponieważ nie wykonujesz iteracji przez wiele elementów tablicy. A kiedy już uda Ci się zrozumieć, jak działają filter
, map
i reduce
, kod również nie jest dużo trudniejszy do zrozumienia.
To nie oznacza, że w kodzie funkcyjnym zawsze używasz map
, reduce
i tym podobnych. Nie oznacza to również, że do zrozumienia map
czy reduce
potrzebne jest programowanie funkcyjne. Chodzi o to, że kiedy tworzysz abstrakcję zastępujące pętle, te funkcje pojawiają się dość często.
Funkcje lambda
Kiedy mówi się o historii programowania funkcyjnego, wielu zaczyna od wynalezienia funkcji lambda. Ale chociaż lambdy są bez wątpienia kamieniem węgielnym programowania funkcyjnego, nie są główną przyczyną jego powstania.
Funkcje lambda są narzędziami, które mogą być użyte do uczynienia programu funkcyjnym. Ale możesz ich również używać w programowaniu obiektowym.
Statyczne typowanie
Powyższy przykład nie jest statycznie typowany. A jednak jest funkcyjny.
Mimo że statyczne typowanie dodaje dodatkową warstwę bezpieczeństwa do Twojego kodu, nie jest to niezbędne, aby był on funkcyjny. Może to być jednak miły dodatek.
Niektóre języki są bardziej funkcyjne niż inne
Perl
Perl ma zupełnie inne podejście do efektów ubocznych niż większość języków programowania. Zawiera on magiczny argument $_
, który sprawia, że efekty uboczne są jedną z jego podstawowych cech. Perl ma swoje zalety, ale nie próbowałbym z nim programowania funkcyjnego.
Java
Życzę Ci powodzenia w pisaniu funkcyjnego kodu w Javie. Nie tylko połowa twojego programu będzie składać się ze statycznych słów kluczowych, ale również większość programistów Javy nazwie twój program haniebnym.
Co nie znaczy oczywiście, że Java jest zła. Nie jest jednak stworzona do problemów, które najlepiej rozwiązywać za pomocą programowania funkcyjnego, takich jak zarządzanie danymi czy aplikacje uczenia maszynowego.
Scala
Ten język jest dość interesujący. Celem Scali jest zjednoczenie programowania obiektowego i funkcyjnego. Jeśli wydaje Ci się to dziwne, to nie tylko Tobie: podczas gdy programowanie funkcyjne dąży do całkowitego wyeliminowania efektów ubocznych, programowanie obiektowe stara się utrzymać je wewnątrz obiektów.
W związku z tym wielu programistów postrzega Scalę jako język, który pomoże im przejść z programowania obiektowego na funkcyjne, a to może ułatwić im przejście do pełnej funkcyjności w późniejszych latach.
Python
Python aktywnie zachęca do programowania funkcyjnego. Można to zauważyć po tym, że każda funkcja ma domyślnie co najmniej jedno wejście, self
. To jest bardzo à la Zen Pythona: jawne jest lepsze niż niejawne!
Clojure
Według jego twórcy Clojure jest w około 80% funkcyjny. Wszystkie wartości są domyślnie niezmienne, tak jak potrzebujesz ich w programowaniu funkcyjnym. Można to jednak obejść, używając wraperów, które mogą się zmieniać, na niemutowalnych danych. Kiedy otworzysz taki wrapper, to co z niego wyjdzie, jest znowu niezmienne.
Haskell
Jest to jeden z niewielu języków, które są czysto funkcyjne i statycznie typowane. Choć może się to wydawać czasochłonne podczas tworzenia programu, bardzo się to opłaca podczas jego debugowania. Nie jest tak łatwy do nauczenia jak inne języki, ale zdecydowanie warto w niego zainwestować!
Nadchodzi Big Data. I przyprowadza przyjaciela: programowanie funkcyjne
W porównaniu do programowania obiektowego programowanie funkcyjne jest wciąż zjawiskiem niszowym. Jeśli włączenie zasad programowania funkcyjnego do Pythona i innych języków ma jakiekolwiek znaczenie, to programowanie funkcyjne wydaje się zyskiwać na popularności.
To ma sens: programowanie funkcyjne jest świetne dla dużych baz danych, programowania równoległego i uczenia maszynowego. A wszystkie te rzeczy przeżywały swój boom w ciągu ostatniej dekady.
O ile kod w programowaniu obiektowym ma niezliczone zalety, o tyle zalety kodu funkcyjnego nie powinny być lekceważone. Nauczenie się kilku podstawowych zasad może często wystarczyć, aby poprawić swój warsztat programisty i aby być gotowym na przyszłość.
Dzięki za przeczytanie tego artykułu!
Oryginał tekstu w języku angielskim przeczytasz tutaj.