Podstawy Programowania Funkcyjnego Epizod 1

michalkulinski.blogspot.com 5 lat temu

O czym jest programowanie funkcyjne?

Zakładam, iż słyszałeś już kiedyś o programowaniu funkcyjnym. No cóż, któż nie słyszał? Wszyscy o tym gadają. Wychodzi dużo nowych języków funkcyjnych takich, jak Scala, F# i Clojure. Ludzie rozmawiają też o starszych językach jak Erlang, Haskell, ML i innych.
A więc, o co w tym wszystkim chodzi? Dlaczego programowanie funkcyjne jest Następną Wielką Rzeczą™? I co jest w tym takiego pociągającego?

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


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


Po pierwsze, prawie na pewno programowanie funkcyjne jest następną wielką rzeczą. Są ku temu dobre, solidne powody i poznamy je w tym artykule. Ale najpierw, aby zrozumieć te powody, musimy poznać, czym programowanie funkcyjne jest. Wkurzę wielu ludzi moim kolejnym stwierdzeniem, ponieważ zamierzam uciec się do skrajnego minimalizmu. Ograniczę programowanie funkcyjne do jego najgłębszej istoty; i to nie jest do końca w porządku, bo temat jest bogaty, obszerny i pełen fantastycznych pomysłów. Ogarniemy te pomysły w przyszłych artykułach. Zobaczysz niektóre z nich już tutaj. Ale na razie, zdefiniuję programowanie funkcyjne w taki sposób:
Programowanie funkcyjne jest programowaniem bez instrukcji przypisania.
O nie! A jednak to zrobiłem. Teraz programiści funkcyjni zbierają się przeciwko mnie, trzymając w rękach widły i pochodnie. Chcą mojej głowy za wypowiedzenie tej minimalistycznej blasfemii. Tymczasem wszyscy goście, którzy mieli nadzieję nauczyć się czegoś o programowaniu funkcyjnym, naprawdę przestali czytać z powodu powyższego zdania, które jest tak rażąco głupie. To znaczy: jak w ogóle można programować bez instrukcji przypisania? Najlepszym sposobem pokazania tego jest przykład. Spójrzmy na bardzo prosty program w Javie: Kwadraty liczb całkowitych.

public
class
Squint {
  public
static
void  main(String  args[]) {
    for  (int  i=1;  i<=25;  i++)
      System.out.println(i*i)
  }
}



Któż z nas nie napisał tego lub czegoś bardzo podobnego? Ja chyba pisałem to setki razy. To jest zwykle drugi program, który piszę w języku, którego się uczę i trzeci czy czwarty w kolejności, gdy uczę młodych programistów pisać programy. Każdy zna stare, dobre kwadraty liczb całkowitych!
Ale przyjrzyjmy się temu uważnie. To jest tylko zwykła pętla ze zmienną nazwaną i, która liczy od 1 do 25. Każde przejście pętli sprawia, iż zmienna i przybiera nową wartość. To jest przypisanie. Nowa wartość jest przypisywana do zmiennej i podczas każdego przejścia pętli. Gdybyś mógł jakoś podejrzeć pamięć komputera i dojrzeć miejsce w pamięci, które przechowuje wartość zmiennej i, zauważyłbyś, iż ta wartość zmienia się podczas każdej iteracji przejścia pętli.
Jeżeli poprzedni paragraf wydawał się rozwodzić się nad oczywistością, to tylko powiem, iż całe artykuły naukowe napisano na ten temat. Pojęcia równoważności, wartości i stanu mogą wydawać się nam oczywiste; ale w rzeczywistości są one same w sobie zagadnieniami bardzo obszernymi. Ale zbaczam za bardzo.
Teraz spójrzmy na funkcyjny odpowiednik programu kwadratów liczb całkowitych. Użyjemy do tego Clojure; choć pomysły, które przerobimy, działają tak samo w każdym innym języku funkcyjnym.

(take 25 (squares-of (integers)))
Tak, dobrze czytasz; powiem więcej: to jest program, który wyświetla prawidłowe wartości. jeżeli chcesz zobaczyć wyniki, oto one:

(1 4 9 16 25 36 49 64 ... 576 625)
W tym programie użyto trzech słów. Każde z tych słów odnosi się do funkcji. Nawiasy z lewej strony tych wyrazów znaczą po prostu: zawołaj tę funkcję i potraktuj wszystko po prawej stronie, aż do prawego nawiasu, jako jej parametry.
Funkcja take przyjmuje dwa argumenty, liczbę całkowitą n i listę l. Zwraca pierwsze n elementów listy l. Funkcja squares-of pobiera listę liczb całkowitych i zwraca listę ich kwadratów. Funkcja integers zwraca listę kolejnych liczb całkowitych, zaczynając od 1. To wszystko. Program po prostu pobiera pierwszych 25 elementów listy kwadratów kolejnych liczb całkowitych, zaczynając od 1.
Spójrz na tę linijkę jeszcze raz; ponieważ zrobiłem tam coś bardzo ważnego. Wziąłem trzy oddzielne definicje funkcji i połączyłem je w pojedyncze zdanie. To się nazywa: (jesteś gotowy na słowo klucz?)
Przejrzystość referencyjna
[w tle: Fanfary, serpentyny, konfetti, tłumy szaleją]

Przejrzystość referencyjna znaczy po prostu, iż w danym zdaniu, możesz zmieniać kolejność słów razem z ich definicjami, i nie zmienia się jednocześnie znaczenie tego zdania. Lub, co ważne dla naszych zastosowań, oznacza to, iż możesz zastąpić każde wywołanie funkcji wartością, którą ta funkcja zwraca. Zobaczmy to w akcji.
Wywołanie funkcji (integers) zwraca (1 2 3 4 5 6 ...) No dobra, pewnie nasuwają Ci się od razu pytania, prawda? To znaczy, jak wielka ma to być lista? Prawdziwa odpowiedź na to pytanie jest taka, iż lista ma być taka duża, jak jest potrzeba, żeby była; ale nie myślmy o tym teraz. Powrócimy do tego w następnym artykule. Na ten moment przyjmijmy, iż (integers) zwraca (1 2 3 4 5 6 ...); bo zwraca!
Teraz w naszym programie możemy zastąpić wywołanie funkcji (integers) jej wartością. Program po prostu staje się:

(take 25 (squares-of (1 2 3 4 5 6 ...)))
A tak, zrobiłem to przy użyciu copy paste'a; i to też jest istotny punkt. Przejrzystość Referencyjna jest tym samym co kopiowanie wartości funkcji i wklejanie jej ponad wywołaniem tej funkcji.
Teraz zróbmy następny krok. Wywołanie funkcji: (squares-of (1 2 3 4 5 6 ...)) po prostu zwraca listę kwadratów liczb z listy jej argumentów. Więc ona zwraca: (1 4 9 16 25 36 49 64 ...). o ile zamienimy wywołanie tej funkcji z jej wartością, program stanie się:

(take 25 (1 4 9 16 25 36 49 64 ...))
I oczywiście wartość wywołania tej funkcji to po prostu:

(1 4 9 16 25 36 49 64 ... 576 625)
A teraz popatrzmy na ten program jeszcze raz:

(take 25 (squares-of (integers)))
Zauważ, iż nie ma zmiennych. W rzeczywistości nie ma tam nic innego, tylko trzy funkcje i jedna stała. Spróbuj napisać kwadraty liczb całkowitych w Javie, nie używając ani jednej zmiennej. Oh, jest prawdopodobnie sposób, żeby to zrobić, ale z pewnością nie jest to naturalne i nie czytałoby się tego tak przyjemnie, jak mój program wyżej.
Co ważniejsze, o ile mógłbyś zajrzeć do pamięci komputera i spojrzeć na miejsca w pamięci używane przez mój program, odkryłbyś, iż te miejsca zostałyby zainicjowane w momencie użycia ich przez program; ale ich wartości pozostałyby niezmienne przez resztę czasu wykonania programu. Innymi słowy, żadne nowe wartości nie zostałyby przypisane do tych miejsc.
W rzeczy samej to jest konieczny warunek dla Przejrzystości Referencyjnej, który opiera się na fakcie, iż za każdym razem, kiedy wywołujesz konkretną funkcję, dostajesz taki sam wynik. Fakt, iż pamięć mojego komputera nie zmienia się podczas uruchomienia mojego programu, oznacza, iż wywołanie funkcji (f 1) zwraca zawsze tę samą wartość, niezależnie od tego, ile razu była wywołana. A to oznacza, iż mogę podmienić (f 1) jej wartością, kiedykolwiek się pojawi.
Albo mówiąc jeszcze inaczej: Przejrzystość Referencyjna oznacza, iż żadna funkcja nie może mieć skutków ubocznych. I oczywiście to oznacza, iż żadna zmienna, raz zainicjowana, nie może nigdy zmienić swojej wartości; wszak przypisanie jest sednem skutku ubocznego.
A więc dlaczego to jest takie ważne? Co jest takiego wspaniałego w Przejrzystości Referencyjnej? Gdy wiemy, iż jest możliwe pisanie programów bez przypisań, dlaczego to takie ważne?
Prawie na pewno czytasz ten tekst na ekranie. A jeżeli nie; komputer znajduje się niedaleko. Jak wiele ma rdzeni? Piszę ten artykuł na MacBooku Pro z 4 rzeczywistymi rdzeniami (Mówią, iż ma 8, ale nie polegałbym bardzo na tym całym "nonsensie hyper-threading". Ma cztery). Mój poprzedni laptop miał dwa rdzenie. I ten poprzednio miał tylko jeden. Jedyny wniosek, jaki z tego mogę wysnuć to, iż mój następny laptop będzie miał 8 prawdziwych rdzeni; i następny-następny mógłby mieć już choćby 16.
Biedni inżynierowie hardware'u, którzy nieśli nas na plecach przez ostatnie cztery dziesięciolecia, osiągnęli w końcu prędkość światła. Zegary komputerów po prostu nie będą poruszały się już znacząco szybciej. Po tym, jak ta prędkość podwajała się co 18 miesięcy, przez okres dłuższy niż większość programistów żyje (oprócz mnie), gwałtowny wzrost prędkości komputerów zatrzymał się, jak dotychczas nie ruszając się znowu.
A więc Ci inżynierowie sprzętu, w pogoni za zaoferowaniem nam coraz większej i większej ilości cykli na sekundę, zaczęli dodawać coraz więcej i więcej procesorów do naszych układów; i nie widać odpowiedzi na pytanie: do ilu procesorów w jednym układzie doprowadzi nas ten marsz ku przyszłości.
A więc pozwól, iż zapytam Ciebie, O zdolny i kompetentny programisto: Jak zapewnisz sobie przewagę w wykorzystaniu cykli dostępnego Ci procesora, kiedy Twój komputer będzie miał 4096 rdzeni w środku? Jak zarządzisz wywołaniami swoich funkcji, o ile będą one wszystkie działać na 16384 procesorach na tej samej szynie pamięci? Jak zbudujesz responsywne i przygotowane na zmiany strony internetowe, kiedy Twoje modele, kontrolery i widoki będą musiały współdzielić 65536 procesorów?
Mówiąc szczerze, my programiści ledwo umiemy sprawić, by dwa wątki w Javie współpracowały ze sobą. Wątki to kaszka z mleczkiem dla niemowlaka w porównaniu ze schabowym z ziemniakami rzeczywistej rywalizacji procesorów na szynie pamięci. Przez ostatnie ponad pół wieku programistom udał się zaobserwować, iż procesy odpalone na komputerze są współbieżne, a nie jednoczesne. A więc chłopcy i dziewczęta, witamy w fantastycznym świecie jednoczesności! A teraz jak sobie z tym dacie radę?
Odpowiedź na to pytanie jest prosta: Porzućcie wszelkie przypisanie, wy, którzy tu wchodzicie.
To jasne, iż o ile wartość w miejscu w pamięci, raz zainicjowana, nie zmienia się podczas wykonywania programu, to nie ma niczego, o co te 131072 procesorów mogłoby rywalizować. Nie potrzebujesz semaforów, jeżeli nie masz skutków ubocznych! Nie masz problemów współbieżnych aktualizacji (przepraszam: Jednoczesnych Aktualizacji), jeżeli w ogóle nie masz aktualizacji!
Więc to jest ta wielka sprawa w językach funkcyjnych; i to jest naprawdę cholernie wielka sprawa. W naszym kierunku pędzi pociąg towarowy, załadowany po brzegi rdzeniami; i lepiej, żebyś był gotowy do czasu jego przyjazdu.



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


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

Następny odcinek tu.
Idź do oryginalnego materiału