Programowanie/Programowanie funkcyjne
Programowanie funkcyjne
edytujJęzyki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi językami programowania, ponieważ zawierają sekwencje zadań do wykonania. Programista jawnie, krok po kroku definiuje jak wykonać zadanie. Programowanie funkcyjne działa inaczej. Zamiast sekwencyjnie wykonywać zadania, języki funkcyjne wyznaczają jedynie wartości poszczególnych wyrażeń.
Programy funkcyjne składają się jedynie z funkcji. Funkcje są podstawowymi elementami języka funkcyjnego. Główny program jest funkcją, której podajemy argumenty, a w zamian otrzymujemy wyznaczoną wartość – wynik działania programu. Główna funkcja składa się tylko i wyłącznie z innych funkcji, które z kolei składają się z jeszcze innych funkcji. Funkcje takie dokładnie odpowiadają funkcjom w czysto matematycznym znaczeniu – przyjmują pewną liczbę parametrów i zwracają wynik. Każda operacja wykonywana podczas działania funkcji, a nie mająca związku z wartością zwracaną przez funkcję to efekt uboczny (np. operacje wejścia wyjścia, modyfikowanie zmiennych globalnych). Funkcje które nie posiadają efektów ubocznych nazywane są funkcjami czystymi (pure function).
Języki czysto funkcyjne
edytujGłównym założeniem języków czysto funkcyjnych jest to, że wynik działania funkcji jest uzależniony od przekazanych jej argumentów i tylko od nich. Nie ma efektów ubocznych. Programy funkcyjne nie zawierają przypisań, więc wartości zmiennych raz ustalone, nigdy nie mogą zostać zmienione. Może to brzmieć dziwnie dla osób przyzwyczajonych do programowania imperatywnego (gdzie działanie większości kodu ogranicza się do modyfikacji wartości zmiennych), ale jest całkowicie naturalne. Zmienna to nazwa powiązana z wartością, w przeciwieństwie do języków imperatywnych, gdzie jest to abstrakcyjne odniesienie do komórki pamięci. Jeśli zmienne są postrzegane jako „skróty” dla wartości (dokładnie tak jak w matematyce), wtedy staje się oczywistym, że ich modyfikacje są niedozwolone. Nie oczekujemy przecież, żeby przypisanie „4=5” było poprawne w jakimkolwiek języku programowania, dlatego jest też dziwnym „x=4; x=5;”.
Funkcja nie robi nic poza wyznaczeniem wartości, którą zwraca. Usunięcie efektów ubocznych pozwala na wyznaczanie wartości wyrażeń w dowolnej kolejności. Funkcja zawsze, bez wyjątków, zwróci taką samą wartość, jeśli przekazane zostaną te same parametry. Ponieważ wartość wyrażenia może być wyznaczana w dowolnym momencie, możliwe jest zastępowanie zmiennych wartościami i odwrotnie. Nazywane jest to referencyjną przeźroczystością (ang. “referentially transparent”). Taki determinizm pozwala wyeliminować całą klasę błędów często popełnianych w programach imperatywnych oraz sprawia, że programowanie funkcyjne jest bardziej „matematyczne” niż programowanie imperatywne. Powyższy opis programowania funkcyjnego mówi więcej o tym czego w programowaniu funkcyjnym nie ma (brak przypisań, brak efektów ubocznych, brak sterowania przepływem danych), niż o tym czym programowanie funkcyjne jest. Dzięki ograniczeniom programy funkcyjne często powstają dużo szybciej. W programach imperatywnych około 90% zawartości kodu to przypisania, których w językach funkcyjnych nie ma.
Wydaje się nielogicznym, aby język programowania stał się silniejszym dzięki ograniczeniu lub wyeliminowaniu niektórych możliwości tego języka. Można jednak zauważyć analogie pomiędzy programowaniem strukturalnym, a programowaniem funkcyjnym. Najbardziej charakterystyczną zaletą programowania strukturalnego jest to, że programy strukturalne nie zawierają instrukcji skoku – goto. Bloki w programowaniu strukturalnym nie mają wielokrotnych wejść lub wyjść. Programowanie strukturalne jest bardziej „matematyczne” niż programowanie niestrukturalne. Takie zalety programowania strukturalnego w porównaniu do niestrukturalnego są podobne do przedstawionych wcześniej zalet programowania funkcyjnego. Tymi zaletami są dodatkowe ograniczenia, które pozwalają pisać bardziej niezawodny kod.
Modularność
edytujNajważniejszym atutem programowania strukturalnego w stosunku do niestrukturalnego jest to, że programy strukturalne mogą być zaprojektowane w sposób modularny. Modularność niesie za sobą znaczny wzrost wydajności pracy programistów. Małe moduły mogą być napisane łatwiej i szybciej. Czasami mogą być wielokrotnie wykorzystywane, dzięki czemu pomagają w szybkim tworzeniu oprogramowania. Niewielkie moduły mogą być testowane niezależnie, co znacznie ułatwia i przyspiesza proces testowania. Podczas pisania programów złożonych z modułów, aby rozwiązać problem najpierw musimy podzielić go na podproblemy, następnie rozwiązać te podproblemy, a później połączyć moduły w jedną całość. Sposób w jaki możemy podzielić problem zależy od tego w jaki sposób będziemy mogli go później połączyć. Aby zwiększyć możliwości wykorzystania modularności konieczne jest dostarczenie pewnego rodzaju „kleju” pozwalającego na połączenie poszczególnych elementów. To jak ważny jest podział problemu na mniejsze części oraz odpowiednie połączenie elementów może zobrazować następujący przykład. Krzesło może być łatwo wykonane poprzez wykonanie części – siedzenie, nogi, oparcie – oraz odpowiednie połączenie ich. Bardzo ważna jest tutaj umiejętność łączenia elementów i to jakiego kleju użyjemy. Brak takiej umiejętności powoduje, że jedynym sposobem wykonania krzesła jest wyrzeźbienie go z jednego dużego bloku drewna, co jest znacznie trudniejsze i bardziej czasochłonne. Ten przykład pokazuje olbrzymią siłę modularności i posiadania „właściwego kleju”. Programowanie funkcyjne w naturalny sposób wymusza modularność i zarazem dostarcza nowe sposoby łączenia modułów w jedną całość.
Funkcje
edytujFunkcje są bardzo ważną częścią funkcyjnych języków programowania. Funkcje są traktowane jak wartości, tak samo jak Int lub String. Funkcja może zwracać inną funkcję, może przyjmować funkcję jako parametr, może być skonstruowana jako połączenie dwóch funkcji. Daje to olbrzymie możliwości łączenia poszczególnych modułów w jeden program. Funkcja, która oblicza wartość pewnego wyrażenia, może brać udział w obliczeniach na przykład jako argument, czyniąc w ten sposób funkcje jeszcze bardziej modularnymi.
Proste funkcje mogą również tworzyć inne, bardziej złożone funkcje. Można na przykład zdefiniować funkcję „differentiate”, która oblicza numerycznie pochodną innej funkcji. Jeśli dana jest funkcja „f”, można zdefiniować funkcję „f’ = differentiate f”, oraz używać ją tak, jak normalnie w kontekście matematycznym. Takie funkcje są nazywane funkcjami wyższego rzędu.
Krótki przykład z języka Haskell przedstawia funkcję numOf, która zlicza elementy z listy które spełniają pewien warunek.
numOf p xs = length (filter p xs)
Powyższa linia mówi: „Aby otrzymać wynik, przefiltruj listę xs za pomocą filtra p i oblicz jej długość”. „p” jest funkcją, która przyjmuje element, oraz zwraca prawdę lub fałsz, określając czy element przeszedł test. Funkcja numOf jest więc funkcją wyższego rzędu – część jej funkcjonalności jest przekazana jako argument.
Bardziej specjalizowana funkcja może wyglądać tak:
numOfEven xs = numOf even xs
Została tutaj zdefiniowana funkcja numOfEven, która wyznacza liczbę parzystych elementów w liście xs. Nie jest konieczne jawne deklarowanie xs jako parametru. Równie dobrze można napisać
numOfEven = numOf even
Kolejna funkcja oblicza liczbę elementów większych lub równych 5 w liście:
numOfGE5 = numOf (>=5)
Funkcją testową jest tu „>=5”, funkcja ta jest przekazana do funkcji numOf aby nadać jej wymaganą funkcjonalność.
Modularność programowania funkcyjnego pozwala na definiowanie standardowych funkcji, do których część funkcjonalności jest przekazywana jako argument. Funkcje takie mogą być wykorzystywane do definiowania skrótów do dowolnej specjalizowanej funkcji. Przedstawione przykłady są trywialne. Przepisanie definicji dla wszystkich przedstawionych powyżej funkcji, nie było by trudne, jednak dla bardziej skomplikowanych funkcji mechanizm ten bywa bardzo wygodny. Na przykład można napisać tylko jedną funkcję sortującą elementy i przekazać część funkcjonalności jako parametr (funkcję porównującą elementy). Rozwiązanie takie pozwoli na posortowanie każdego typu danych dostarczając jedynie funkcję porównującą elementy. Jeśli poświęcimy trochę czasu, aby upewnić się, że ogólna funkcja działa poprawnie, mamy pewność, że specjalizowane funkcje też działają poprawnie.
Podobne rozwiązania są dostępne również w niektórych imperatywnych językach programowania (np. w postaci szablonów C++). Różnica jest taka, że rozwiązanie zastosowane np. w języku Haskell jest znacznie bardziej intuicyjne i eleganckie, dlatego jest używane częściej.
Podsumowanie
edytujPonieważ języki funkcyjne oferują więcej intuicyjnych sposobów zrealizowania zadania, programy funkcyjne są zwykle krótsze (często 2 do 10 razy). Semantyka, w porównaniu do wersji imperatywnej, jest często bardziej zbliżona do problemu, co sprawia, że weryfikacja poprawności działania funkcji jest łatwiejsza. Języki czysto funkcyjne nie pozwalają na stosowanie efektów ubocznych, dzięki czemu liczba błędów jest mniejsza. Programy takie są zatem prostsze do napisania, bardziej niezawodne i łatwiejsze do utrzymania.