Kiedy warto stosować programowanie funkcyjne?

0

Jak uważacie, w jakich sytuacjach programowanie funkcyjne jest optymalnym paradygmatem do rozwoju oprogramowania?

Cokolwiek przeczytam w internecie na ten temat to bardziej artykuły podkreślą łatwiejsze rozwiązywanie problemów związanych z współbieżnością lub łatwiejszym analizowaniem kodu, bo w czystych funkcjach nie ma efektów ubocznych więc jest mniej okoliczności, gdzie coś może pójść nie tak po naszej myśli.

Ale to do mnie niezbyt przemawia, albo inaczej, to ukazuje FP w dość słabym świetle, jako niszowy i bardzo niepraktyczny paradygmat, ponieważ większość aplikacji nie ma problemów z współbieżnością chyba, że 1) używa się ram, które mają magię, której nie idzie łatwo zrozumieć i od której nie można uciec lub 2) używa zbyt ograniczonych baz, które nie obsługują transakcji w stopniu w jakim wymaga aplikacja.

Wystarczy mieć posgresql i stosować pewniejsze ramy lub nawet brak ram, a większość potencjalnych problemów jakie rozwiązuje FP wydaje się na pierwszy rzut oka niedostrzegalne i nie warte podjęcia wysiłku.

Głowiłem się nie raz po co używać FP, tym bardziej gdy w komercyjnym projekcie pewne rzeczy się wykluczają jak np. używanie FP i postgresql. Tutaj transakcje mają charakter imperatywny. Próba pisania tego zgodnie z FP jest kuriozalna. Oczywiście zapytania są deklaratywne, ale blokady czy logika sterująca między nimi już nie więc robienie z tego czegoś deklaratywnego totalnie mija się z celem.

Dopiero z czasem zobaczyłem pewną korzyść jaka idzie w parze z FP, ale jeśli trochę się odpuści i stosuje FP tam gdzie ma większy sens.

Tutaj chciałem podzielić się moimi uwagami, myślę, że spostrzeżenie jest o tyle ciekawe, że sam wcześniej nie przeczytałem niczego podobnego/zbliżonego. Wydaje mi się, że ta rzecz powinna być podkreślana już na samym początku nauki funkcyjnych języków.

W przypadku FP, warto podkreślić, że jest to paradygmat jaki sprzyja właściwie przetwarzaniu gruboziarnistemu. W innych paradygmatach też dane można przetwarzać gruboziarniście, ale różnica w FP polega na tym, że tutaj mamy więcej opcji do zredukowania złożoności końcowego rozwiązania.

Klasycznie, by ogarnąć złożoność, kontrolować ją, posiłkujemy się różnymi abstrakcjami. Za abstrakcję uznaje się ten kod, który nie jest kodem biznesowym i który pod wpływem zmian wymań biznesowych projektu nie ulega zmianie. Czyli to taki niezmienny kod, którego definicja jest niemal stała. Dobrym przykładem są funkcje takie jak sort, min, max, sum.

Sytuacja w przypadku FP wypada o tyle ciekawiej, że więcej rzeczy można wyabstrahować, a to dlatego, że tworzenie kolekcji z innej kolekcji jest znacznie tańsze. Wynika to z niemodyfikowalności danych, w tym kolekcji i optymalizacji związanej z persistent data structures. Dzięki temu można pisać bez większych problemów rozbudowane wyrażenia z zagnieżdżonymi kolekcjami, które w efekcie końcowym mogą doprowadzić do powstania jakiś rozdmuchanych dokumentów choćby pdf, xml, html :D (wygodnie i to bez korzystania z silnika szablonów, co jest dla mnie pewnym odkryciem).

Wg mnie tym co warto podkreślić to właściwie wszystkie pośrednie operacje na zagnieżdżonych kolekcjach jakie tutaj można wyabstrahować, i użyć podczas pisania kodu biznesowego. Taki krok z jednej strony niweluje konieczność pisania wielu pętli w kodzie biznesowym, a z drugiej prowadzi do powstania kodu jaki można ponownie użyć.

Składanie operacji, bez pisania pętli sprzyja pracy na styku gdzie są zebrane dane i potrzeba analiza. Czyli w takich językach jak M, R czy J (osobiście w żadnym nie programowałem, ale one od razu odsłaniają to czego na początku nie widziałem). Myślę, że tego typu języki powinny być pierwszym wyborem jeśli ktoś chce spróbować z funkcyjnym programowaniem.

1

Niektóre zasady z FP warto stosować wszędzie. Dla przykładu, mi się bardzo podoba pomysł nie redefiniowana wartości zmiennych (tzn raz ustalona, nie zmienia wartości, to samo z polami w obiektach) - i staram się to stosować wszędzie.

I szczerze mówiąc, poprawcie mnie jeśli się mylę, ale tylko tyle wystarczy żeby pisać funkcyjnie? Po prostu nigdy nie przypisuj zmiennej wartości więcej niż raz, i napisz cały kod tak żeby był immutable, wsadź IO w monadę, i możesz pisać funkcyjnie nawet w Pascalu.

Lambdy i flat mapy to jak rozumiem taki dodatek, konsekwencja wynikająca z tego? Taki dodatek do języka.

0

Jak dla mnie są trzy możliwe powody, żeby nie używać FP: (1) Nie rozumienie go; (2) Nie można go zastosować (np. nie możemy użyć języka, który daje dobre wsparcie); (3) rozwiązujemy problem ad-hoc i imperatywne rozwiązanie jest trywialne. Jak się nie umie myśleć w kategoriach FP to rzeczywiście programowanie w ten sposób jest wyzwaniem, ale gdy się załapie to funkcyjnie jest po prostu łatwiej. Nie trzeba mieć z tyłu głowy wielu czynników, które mogą nie wynikać z kodu w oczywisty sposób, łatwiej testować i refaktorować. Co więcej rozwiązania funkcyjne, dobrze zrobione, są znacznie bardziej eleganckie i bardziej cieszą oczy.

Co do języków, to nie powiedziałbym, że R jest dobry do programowania funkcyjnego. Tak sobie, bym powiedział. Języki dobre do FP (moim zdaniem, które znam) to Haskell, Rust, Scala.Też Perl całkiem sobie radzi, ale to już nie ta klasa.

0

Nie wiem jak to jest we wszystkich językach, ale w clojure referencje można przysłonić. W zasadzie bardziej chodzi o to, by nie mutować. Generalnie jak zablokujesz mutowanie to właściwie tracisz z miejsca pętle, break, continue. Dla pętli potrzebujesz wystawić reduce, a żeby obejść break/continue leniwe wartościowanie i jakieś api do obracania wygenerowanymi sekwencjami.

elwis napisał(a):

Jak dla mnie są trzy możliwe powody, żeby nie używać FP: (1) Nie rozumienie go; (2) Nie można go zastosować (np. nie możemy użyć języka, który daje dobre wsparcie); (3) rozwiązujemy problem ad-hoc i imperatywne rozwiązanie jest trywialne. Jak się nie umie myśleć w kategoriach FP to rzeczywiście programowanie w ten sposób jest wyzwaniem, ale gdy się załapie to funkcyjnie jest po prostu łatwiej. Nie trzeba mieć z tyłu głowy wielu czynników, które mogą nie wynikać z kodu w oczywisty sposób, łatwiej testować i refaktorować. Co więcej rozwiązania funkcyjne, dobrze zrobione, są znacznie bardziej eleganckie i bardziej cieszą oczy.

Co do języków, to nie powiedziałbym, że R jest dobry do programowania funkcyjnego. Tak sobie, bym powiedział. Języki dobre do FP (moim zdaniem, które znam) to Haskell, Rust, Scala.Też Perl całkiem sobie radzi, ale to już nie ta klasa.

Nie znam tych języków M, R, J, ale koncepcja gdzie piszesz fragment kodu bez bezpośredniego posiłkowania się pętlą trochę zbliża się do filmów o hackerach lub gui z jakiego korzystają agenci CSI :-P

0
Riddle napisał(a):

Lambdy i flat mapy to jak rozumiem taki dodatek, konsekwencja wynikająca z tego? Taki dodatek do języka.

To zależy czy ktoś poza wyciągnięciem pochodnej rozumie czym ona jest. To jest chyba główny argument dlaczego programowanie funkcyjne zazwyczaj nie warto stosować.

0
znowutosamo6 napisał(a):

Generalnie jak zablokujesz mutowanie to właściwie tracisz z miejsca pętle, break, continue. Dla pętli potrzebujesz wystawić reduce, a żeby obejść break/continue leniwe wartościowanie i jakieś api do obracania wygenerowanymi sekwencjami.

wszelakie pętle można zastąpić rekurencją ogonową. z drugiej strony, wstawianie rekurencji ogonowej gdzie popadnie strasznie by zaśmieciło kod i dlatego funkcyjne kolekcje (i inne funkcyjne typy danych) mają wiele kombinatorów (map, flatmap, reduce, groupby, filter, itp itd), które załatwiają blisko 100% zastosowań.

0

Tam, gdzie dużo logiki. Całe szczęście FP do tego celu można używać w każdym języku programowania. Nawet w takim pythonie mogę operować na niemutowalnych strukturach danych. Co prawda funkcje w śroku mogą być mutowalne, ale największą zaletą FP to właśnie wymuszone immutable, które sprawia, że design i przepływ programu jest prosty i testowalny

Riddle napisał(a):

Lambdy i flat mapy to jak rozumiem taki dodatek, konsekwencja wynikająca z tego? Taki dodatek do języka.

flatMap to warunek konieczny do monad. Sama monada to abstrakcja na temat wykonywania sekwencyjnych (to bardzo ważne) przekształceń, gdzie kolejne przekształcenia to właśnie flatMap.

1

Głowiłem się nie raz po co używać FP, tym bardziej gdy w komercyjnym projekcie pewne rzeczy się wykluczają jak np. używanie FP i postgresql.

Czemu niby się wykluczają? Absolutnie to nie jest prawda.

Ale to do mnie niezbyt przemawia, albo inaczej, to ukazuje FP w dość słabym świetle, jako niszowy i bardzo niepraktyczny paradygmat, ponieważ większość aplikacji nie ma problemów z współbieżnością chyba, że 1) używa się ram, które mają magię, której nie idzie łatwo zrozumieć i od której nie można uciec

No RAM zawsze musisz użyć, w taki czy inny sposób. Natomiast to co FP Ci daje to, że łatwiej jest śledzić przepływ danych w aplikacji, gdyż jedyny sposób w jaki funkcja może wpłynąć na dane jakie masz, to zwrócenie ich w zmodyfikowanej formie. Innymi słowy:

data_copied = deep_copy(data)

f(data)

data == data_copied

Dla każdej funkcji f. Jest to rzecz, która znacząco ułatwia debuggowanie i testowanie aplikacji.

Do tego jeszcze jak masz bardziej rozbudowany system, to często też masz dodatkowe rzeczy, które robisz - np. współdzielone cache. W takim przypadku musisz uważać, by nie zmodyfikować przypadkowo danych we współdzielonym cache. Praktycznie każda większa współczesna aplikacja używa gdzieś współbieżności by mieć odpowiednią wydajność.

  1. używa zbyt ograniczonych baz, które nie obsługują transakcji w stopniu w jakim wymaga aplikacja.

Nie wiem w jaki sposób dobór DB ma tutaj jakiekolwiek znaczenie.

ale koncepcja gdzie piszesz fragment kodu bez bezpośredniego posiłkowania się pętlą trochę zbliża się do filmów o hackerach lub gui z jakiego korzystają agenci CSI

Niby czemu?


Dobrym przykładem gdzie FP się bardzo dobrze spisuje są wszelkiego rodzaju aplikacje sieciowe, ponieważ większość z nich działa na zasadzie:

request -> process -> response

A wiesz co to przypomina? Aplikację funkcji w postaci process :: Request -> Response (± jakieś monady czy inne rzeczy by zadowolić system typów jeśli takowy mamy). Po prawdzie to dotyczy nie tylko aplikacji sieciowych, ale bardzo dużo innych systemów po prawdzie sprowadza się do tego prostego schematu. Więc jak widać większość serwerów bardzo ładnie pasuje nam do systemów FP.

No ale co z systemami, które np. utrzymują połączenie dłużej i są stanowe? No to też można łatwo zamodelować używając FP, bo dalej one opierają się na prostym request -> process -> response z tą różnicą, że jednocześnie trzymamy jakiś stan przez chwilę, więc mamy funkcję process :: State -> Request -> (State, Response) (czy podobną, w zależności już tutaj od konkretnego języka, ale sprowadza się to dalej do tego samego).

1
znowutosamo6 napisał(a):

Jak uważacie, w jakich sytuacjach programowanie funkcyjne jest optymalnym paradygmatem do rozwoju oprogramowania?

programowanie funkcyjne rozwiązuje podobne problemy, które występują w innych rodzajach programowania, ale które inne rodzaje programowania rozwiązują w inny sposób.

jednym z tych problemów jest shared mutable state, czyli że jeśli masz pewien stan (zmienną, dane itp.) które się zmieniają i które są wykorzystywane w wielu miejscach w kodzie, to ciężko nad tym zapanować.

W OOP(ale i w proceduralnym paradygmacie można tak pisać) dąży się do tego, żeby ograniczyć miejsca w kodzie, gdzie następuje zmiana/odczyt danych - stosuje się hermetyzację (czyli zmienianie danych ma być ograniczone do obiektu, inne obiekty nie mają prawa dostępu do tych danych), programowanie sterowane przez eventy, wzorzec CQRS (Command and Query Responsibility Segregation - czyli oddzielamy mutację i odczyt) itp.

Czyli chodzi o to, żeby zapewnić, że dane będą się zmieniały tylko w kontrolowany sposób i żeby nie były rozsynchronizowane (bo jak masz jakiś kawałek kodu, który zmienia jakieś dane, to potem może się rozjechać, jeśli inny kawałek kodu będzie korzystał z tych samych danych w niekontrolowany sposób). Tutaj podoba mi się Rust, bo zawiera on mechanizmy na poziomie języka, które są w stanie tego pilnować, żeby dane były zmieniane w kontrolowany sposób.

Natomiast programowanie funkcyjne kasuje ten problem zmiany danych i używa niemutowalności (czyli raz ustawione dane się nie zmieniają. Wow, cała klasa problemów skasowana, bo po co zmieniać dane, jak można ich nie zmieniać? 💡). Plus stosuje jakieś bardziej zaawansowane mechanizmy i abstrakcje (nie będę się rozwodził nad nimi, bo je słabo znam).

postgresql. Tutaj transakcje mają charakter imperatywny.

Transakcje w bazach danych też można określić jako pewną kontrolę tego, żeby zmiany następowały w ściśle określony sposób.

Wystarczy mieć posgresql i stosować pewniejsze ramy lub nawet brak ram, a większość potencjalnych problemów jakie rozwiązuje FP wydaje się na pierwszy rzut oka niedostrzegalne i nie warte podjęcia wysiłku.

Ale czy jedno drugiemu przeszkadza? Baza danych to peryferia, a nie cała apka (chyba, że robisz coś, co faktycznie opiera się tylko i wyłącznie na bazie danych).

1 użytkowników online, w tym zalogowanych: 0, gości: 1