Nie rozumiem sensu programowania funkcyjnego

Odpowiedz Nowy wątek
2019-07-29 21:55
2

I podobnie jak w wątku poniżej nie chcę się kłócić, że "funkcyjne jest be", tylko naprawdę nie rozumiem.

Ostatnio mamy wielki trend który głosi, że "Programowanie funkcyjne jest dużo lepsze niż nie-funkcyjne". A w szczególności - lepsze niż wszystko, co imperatywne.

Niech im będzie. Ale dla mnie programowanie funkcyjne jak i argumentacje za nim sa naprawdę trudne do przełknięcia.

Po pierwsze: Dla mnie osobiście jest ono bardzo nieintuicyjne.

Ktoś na reddicie twierdził, że nieintuicyjność FP być może wypływa tylko z tego, że ludzie od początku są szkoleni do myślenia imperatywnego. Gdyby wyrzucić języki imperatywne ze szkół i zastąpić je funkcyjnymi, to myślenie funkcyjne byłoby równie intuicyjne.

Ja natomiast osobiście odnoszę wrażenie, że po prostu informatyka byłaby jeszcze bardziej znienawidzona przez większość uczniów.

Wydaje mi się, że dla człowieka naturalne jest myślenie czasownikami i w kategoriach zmieniającego się stanu.

Imperatywnie: Piszę na kartce zdanie Ala ma kota pod akapitem, który napisałem wcześniej.

Funkcyjnie: Niech k będzie dowolną kartką. k' jest kartką taką, że cały tekst napisany na k' jest identyczny z tekstem napisanym na k, a dodatkowo na k' jest jeszcze zdanie Ala ma kota poniżej jakiegokolwiek innego tekstu.

Nawet pisze się koślawo, co wskazuje, że język naturalny nie powstał do tego rodzaju opisu (bo ludzie tak nie myślą). Językiem ojczystym będzie tu raczej język matematyczny: k' = ιk∈K. tekst(k') = tekst(k) || "Ala ma kota". Tutaj dopiero to wygląda naturalnie; ale ludzie nie posługują się takim językiem na co dzień.

Gdzie tu są problemy? (a) Wyrzucamy czasowniki, bo one ze swej natury modyfikują stan; zamiast tego mamy myśleć deklaratywnie, a więc opisywać co czym jest, a nigdy: co się z czym odbywa. Jednak ludzie myślą czasownikami. (b) Co za tym idzie: Tworzymy nową kartkę. (WTF? Jak piszę na kartce to nie pojawia mi się stosik kartek po każdym napisanym słowie?) Wyrzucenie czasowników wywołuje eksplozję rzeczowników, wskutek czego zdawałoby się jeden i ten sam obiekt nagle opisywany jest wieloma rzeczownikami, a tak w ogóle to są to zupełnie różne obiekty.

Jednak tak nie jest w rzeczywistym życiu, skąd ludzie czerpią intuicje. Krojenie marchewki nie tworzy nowej marchewki. Włączenie telewizora nie powoduje, że w pokoju telewizor włączony zaczyna stać obok identycznego, ale wyłączonego.

Skądinąd zastanawiam się, co by było, gdyby przeprowadzić takie badanie: Wziąć tzw. "zwykłego człowieka" i programistę funkcyjnego i poprosić o wypowiedź na ten sam temat. Zanotować, jaka była u kogo proporcja rzeczowników do czasowników (wyjąwszy "być" i "mieć") oraz proporcja czasowników "być" i "mieć" do innych czasowników. Mam podejrzenia, że programista funkcyjny nawet na co dzień używałby mniej czasowników. Jestem ciekaw, co by z takiego badania wyszło. Bo jeśli moje przypuszczenia są prawdziwe - to jakby potwierdzało to, że bycie programistą funkcyjnym wymaga bardzo głębokich zmian w intuicji.

Po drugie: Współdzielony zmieniający się stan bywa wygodny!!

Na JPP na studiach był prikaz, by napisać interpreter wymyślonego przez siebie języka w Haskellu. To mnie zmusiło, by tego Haskella wreszcie spróbować.

Jakoś nie byłem zachwycony.

Wymyśliłem sobie język (imperatywny, ale z kilkoma nowatorskimi pomysłami). Tak - jest monada State. Pomocna przy takiej zabawie. Wśród licznych obiektów odpowiadających za stan mamy np. listę scope'ów. W moim języczku akurat nie było eksplozji możliwych dowiązań do tego samego scope'u, ale wciąż jednak pod pewnymi warunkami można się było dostać do tego samego scope'u z kilku różnych miejsc.

To natychmiast zaczęło wymagać, by każdy scope miał swój id. Scope nie mógł być dowiązany bezpośrednio do nazwy zmiennej - nie, nazwa zmiennej miała przechowywać id swojego scope'a.

Zamiast zatem prostego przejścia ze zmiennej do scope'a: Pobranie id, pobranie scope'a o tym id ze stanu przechowywanego w monadzie, zrobienie coś z tym scope'em, zapisanie nowego stanu... I tak jednolinijkowiec stawał się co najmniej trójlinijkowcem. (FP ma zmniejszać boilerplate w porównianiu do obiektówki)

I co za tym jeszcze idzie: Trzeba nagle eksplicite w ogóle deklarować tę listę scope'ów! W języku imperatywnym w ogóle takiej listy bym nie stworzył. Zamiast tego bym po prostu dołączał odpowiednie referencje.

(I co jeszcze za tym idzie, choć to już nie problem z mojego projektu, bo w moim języku z innych przyczyn i tak byłoby to niemożliwe: Problemy z automatycznym zarządzaniem pamięcią. Wyobrażam sobie, że gdybym pisał interpreter w nawet tej nieszczęsnej Javie (interpreter normalniejszego języka niż to, co wymyśliłem), to miałbym garbage collecting za darmo. Ginie ostatnia zmienna, która dowiązywała do jakiegoś scope'u? Brak jest dowiązań do scope'u, ginie i scope. W Haskellu? Ponieważ musi być centralny słownik wszystkich scope'ów, to dowiązanie do scope'a jest i wyciek pamięci gotowy.)

Co więcej: to wymuszało, by pojedyczne funkcje były naprawdę bardzo małe i robiły naprawdę tylko jedną rzecz. Dlaczego? Bo po zapisaniu zmian do monady trzeba... Pobrać dane z monady ponownie. Jeśli tego nie zrobię, to będę operował na nieaktualnych danych. To wymusza wyrzucanie każdej operacji pobierz/zapisz do osobnej funkcji, bo inaczej funkcja zaczyna być nieznośna. (Tak, pewnie ktoś zaraz powie, że "to dobrze bo i w obiektówce funkcje winny być jak najmniejsze - ciesz się, programowanie funkcyjne wymusza na tobie, co i tak powinieneś robić.)

Chodzi mi o to, że - jak dla mnie - w FP trzeba bardzo pilnować, by się stan nie rozsynchronizował. Ciągłe tworzenie nowych i jeszcze nowych obiektów łatwo może doprowadzić do tego, że skądś dojdziemy do nieaktualnych danych.

A teraz teoretycznie: Gra. Chociażby ta, którą dłubię. To wydaje się być po prostu w opór imperatywne: Jeden stworek wali drugiego więc drugi traci 20 HP. Chcę wykorzystać fakt, że można modyfikować stan globalny! Bo teraz za darmo mam już to, że to zmniejszenie się HP jest widoczne zewsząd: z listy stworków drużyny pojedynczego gracza, ze stworka, który właśnie zadał cios (referencja o wdzięcznej nazwie opponent), wszytkie efekty które zależą od HP też będą miały dostęp do aktualnego HP... W Haskellu nie miałbym tego. Gdybym po prostu zrobił dowiązania, to te dowiązania zdezaktualizowałyby się z chwilą zadania ciosu. A więc znów - centralny rejestr wszystkich stworków, jakieś sztuczne id tych stworków pewnie (aktualny przeciwnik nie jest zatem stworkiem tylko jakąś dziwną liczbą, na podstawie której trzeba dopiero pobrać stworka), i pewnie jeszcze to samo z wieloma innymi rzeczami, gdzie obecnie wykorzystuję dowiązania.

Globalny stan jest be - ale jak stworek oberwie to ja naprawdę chcę, żeby ze wszystkich innych miejsc w kodzie było widać zmniejszone HP!! A przecież w myśl paradygmatu funkcyjnego to jest właśnie tragedia, jeśli zmiana w jednym miejscu kodu propaguje się nagle do innych miejsc w kodzie. Tyle że dla mnie wyrzucenie takich propagacji wymusza komplikowanie kodu, jak opisałem na przykładach wyżej.

Jednak

Uznaję, że powyższe problemy mogą po prostu wynikać z tego, że nigdy nie nauczyłem się porządnie pisać funkcyjnie. Może po prostu próbuję przenosić praktyki, nawyki i intuicje z jęz. imperatywnych do jęz. funkcyjnych, widzę, że one jakoś nie pasują, więc odrzucam Haskella podczas gdy powinienem odrzucić te nawyki - czyli oduczyć się wszystkiego, co już umiem, i uczyć się na nowo.

Może zatem dla własnego dobra trzeba by się zmusić do pisania fukncyjnego, i powrócenia do tematu za pół roku albo i rok nawet - może po takim ćwiczeniu też bym nagle zaczął dostrzegać same zalety FP i już nigdy bym nie chciał z własnej woli pisać imperatywnie.

Nie za bardzo widzę, jak to zrobić w praktyce. W pracy mam Pythona na razie - nie chcę wykorzystywać zadań z pracy do nauki paradygmatu, z którym na razie nie za bardzo mi po drodze. Ucierpiałaby praca. Uczyć się tego sam dla siebie? Sam dla siebie piszę grę w C# - jak zacznę przepisywać ją na Haskella to chyba nigdy jej nie zrobię. Tzn będzie to może interesujące ćwiczenie pod wzgl. nauki Haskella, ale raczej będzie wymagało porzucenia marzeń o ukończeniu jej.

Tak, niby na upartego w C# DA SIĘ pisać funkcyjnie, ale znów to samo... zbyt wielkie skupienie na tym, JAK to mam robić doprowadzi do tego, że tego nie zrobię. Tym bardziej, że obecnie dostrzegam tylko upierdliwości na myśl o porzuceniu wszelkich side-effectów.

Uczyć się funkcyjnego jako trzecie zadanie, obok pracy i powyższej gry? Sorry.. doba ma 24h.

A jednak przejść nad tym do porządku dziennego nie mogę. Za wiele ludzi śpiewa peany na cześć FP. Naprawdę może być tak, że nie ucząc się go, krzywdzę wyłącznie samego siebie. I z własnego wyboru pozostaję niekompetentny.

W zasadzie nie wiem, co teraz z tym zrobić.

O kurczę, normalnie jestem influencerką ;) - Freja Draco 2019-07-29 22:02
@Freja Draco: Ano, wygląda. :) Ale moim zdaniem to zachęciłaś kilometry na godzinę / @kmph do opisania tego, do czego i tak się już zbierał od pewnego czasu. - Silv 2019-07-30 06:01
Uznaję, że powyższe problemy mogą po prostu wynikać z tego, że nigdy nie nauczyłem się porządnie pisać funkcyjnie. Może się naucz, napisz jakiś projekt w języku czysto funkcyjnym i wtedy się wypowiedz. Co myślisz? - nullpt4 2019-07-30 15:02
@nullpt4: Tak jak pisałem, już zostałem zmuszony do napisania projektu w języku czysto funkcyjnym - na studiach. I tak jak pisałem, z nauczeniem się naprawdę porządnie programować funkcyjnie jest ten problem, że nie za bardzo widzę, jak to zmieścić w dobie... - kmph 2019-07-30 18:01

Pozostało 580 znaków

2019-08-09 10:01
1

To zależy więc nie powiem, że zawsze, ale bardzo często robienie globalnego obiektu gdzie coś zmieniasz i inne obiekty od tego zależą jest słabe, bo powoduje dużo efektów ubocznych, Utrudnia to też testowanie, a komponenty stają się silnie powiązane, a powiązania między częściami systemu powinny być słabe i elastyczne. Już dużo lepiej w tym przypadku zastosować wzorzec Obserwatora - poczytaj o nim. Co do samych efektów ubocznych - nawet pisząc w obiektowym języku, jeśli unikasz niejawnych efektów ubocznych to kod sprawia mniej niespodzianek i błędów z d**y.

Pozostało 580 znaków

2019-08-09 10:20
1

Tutaj jest to wytłumaczone przez kogoś znacznie lepiej obeznanego z tematem niż ja:
https://www.joelonsoftware.co[...]programming-language-do-this/

Pozostało 580 znaków

2019-08-09 10:27
0
stivens napisał(a):

A czasowniki to nie obiektowka? W koncu w obiektowce dodajemy do listy a w FP...

(cons x current_list)

... a w FP wyrazenie wyzej ma wartosc rowna liscie z x'sem w glowie i current_list w ogonie. W szczegolnosci niczego nie dodalismy bo jezeli gdzies jest jeszcze referencja do starej listy to ona o nowej glowie nie wie. Zadnego czasownika tutaj nie ma? Wiec myslimy wartosciami a nie czynnosciami?

zły przykład podałeś, bo akurat cons to skrót od construct czyli czasownik jakby jest.

Co do myślenia czasownikami to oczywiście uproszczenie, ale widzę tą różnicę w modelowaniu systemu - od czego zaczynasz - od diagramu klas/struktur/encji czy od procesów/funkcji/przepływów. (btw. można to też robić w C#/Javie nawet w stylu obiektowym i długo tak robiłem)


Bardzo lubie Singletony, dlatego robię po kilka instancji każdego.
Aa, w tym sensie - stivens 2019-08-09 10:52

Pozostało 580 znaków

2019-08-09 14:02
2

@kmph:

ad 1. bo programowanie funkcyjne często jest uczone matematycznie, a nie praktycznie jak OOP. Dlatego wiele osób ma problem z przeniesieniem elementów FP do projektów biznesowych.
ad 2. Jonh Carmack w 2013 pokazał, jak uczył się Haskella, portując Wolfa 3d na Haskella właśnie → https://www.youtube.com/watch[...]ontinue=127&v=1PhArSujR_A . Później Daniel Holmes podchwycił ideę i samemu napisał W3d w haskellu https://github.com/danielholmes/wolf3d-haskell jak widać stan globalny nie jest potrzebny. Są jak widać lepsze metody.

Poza tym przeczytaj: https://medium.com/better-programming/fp-toy-7f52ea0a947e

Pozostało 580 znaków

2019-08-09 14:04
0

@jarekr000000: chciałbym zobaczyć implementację Age of Empires 2 w haskellu... po prostu moja głowa tego nie ogarnia jak można robic tyle kopi stanów i przekazywac między sobą


Nie pomagam przez PM. Pytania zadaje się na forum.
Pokaż pozostałe 3 komentarze
@Silv: za duże uproszczenie. - Koziołek 2019-08-09 14:18
A tylko w kontekście operacji "usuwania" i "dodawania" elementów tablicy? - Silv 2019-08-09 14:22
@Silv: tak, choć w FP nie można mówić o obiektach. Są pewne struktury danych, na których operujesz. - Koziołek 2019-08-09 14:23
@Koziołek: no, no, to jeszcze się douczę (kiedyś). Dzięki. :) - Silv 2019-08-09 14:24

Pozostało 580 znaków

2019-08-09 14:23
0
scibi92 napisał(a):

@jarekr000000: chciałbym zobaczyć implementację Age of Empires 2 w haskellu... po prostu moja głowa tego nie ogarnia jak można robic tyle kopi stanów i przekazywac między sobą

Akurat IMO tego typu gry bardzo dobrze nadają się pod FP. Ale jak jakas bedzie to zobaczymy.
A poza tym jakich kopii....


Bardzo lubie Singletony, dlatego robię po kilka instancji każdego.
edytowany 3x, ostatnio: jarekr000000, 2019-08-09 14:38
Pokaż pozostałe 6 komentarzy
Login Ci się wstawiał? ;) Ale kombinacja klawiszy czy jak? - Silv 2019-08-09 14:29
autotype - czyli hasło też wysyłałem ... ale po enterze - jarekr000000 2019-08-09 14:33
niezły numer - jarekr000000 2019-08-09 14:34
Dobrze, że już wiesz. - Silv 2019-08-09 14:35
Ktoś tu pisał, że są dużo lepsze języki od pro obiektowych Java i Kotlin, może mi zdradzić jakie? - sanny 2019-08-09 14:52

Pozostało 580 znaków

2019-08-09 17:44
0

chciałbym zobaczyć implementację Age of Empires 2 w haskellu... po prostu moja głowa tego nie ogarnia jak można robic tyle kopi stanów i przekazywac między sobą

@scibi92 a o Entity Component System słyszał? To jak najbardziej funkcyjne podejście do programowania gier (masz eventy, które są składane na obecny stan (komponent) i są emitowane przez systemy, entity to tylko ID, który łączy komponenty dot. tej samej jednostki). Więc jak najbardziej się da i powiedziałbym, że nawet to jest obecnie podejście "zgodnie ze sztuką".

Pozostało 580 znaków

2019-08-09 18:36
0

FP jest dobre, bo łatwo zarządzać kodem i testować go. Także ułatwia skalowanie przez współbieżność. Moim zdaniem trudność polega na tym, że trzeba rozumieć problem, podczas gdy w programowaniu proceduralnym wystarczy wiedzieć jak go rozwiązać. Innymi słowy fp będzie trudne dopóki działasz po omacku. Gdy osiągniesz odpowiedni poziom fp jest oczywiste, choć w niektórych przypadkach zmienny stan może być pożądany choćby ze względu na wydajność. Poza tym, żeby fp sprawiało radość, język musi być odpowiedni, na przykład Java nie jest takim językiem, więc nie zwracam kijem Wisły i używam w nim znacznie mniej elementów fp.


edytowany 1x, ostatnio: elwis, 2019-08-09 18:37

Pozostało 580 znaków

2019-08-17 13:33
0

Utrudnia to też testowanie, a komponenty stają się silnie powiązane, a powiązania między częściami systemu powinny być słabe i elastyczne.

Zagram adwokata diabła. Ten problem rozwiązuje dependency injection i programowanie do abstrakcji (interfejsów). Nie widzę jak miałoby to być coś co rozwiązuje wyłącznie FP. Czasem odnoszę wrażenie że takie argumenty (nie ważne czego tyczy się dyskusja) są używane byle by były, taka sztuka dla sztuki.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.

Pozostało 580 znaków

2019-08-17 13:56
0
Aventus napisał(a):

Utrudnia to też testowanie, a komponenty stają się silnie powiązane, a powiązania między częściami systemu powinny być słabe i elastyczne.

Zagram adwokata diabła. Ten problem rozwiązuje dependency injection i programowanie do abstrakcji (interfejsów). Nie widzę jak miałoby to być coś co rozwiązuje wyłącznie FP. Czasem odnoszę wrażenie że takie argumenty (nie ważne czego tyczy się dyskusja) są używane byle by były, taka sztuka dla sztuki.

Tutaj bardziej chodzi o powiązania w sensie kto komu zmienia stan, a nie kto tworzy obiekty i jak szeroki jest udostępniany interfejs. W kodzie imperatywnym często spotyka się takie coś:

val x = skonstruujNowyX();
obiekt1.ustawCoś(x);
assert(obiekt2.pobierzCoś() == x.name);

Widać tutaj silne powiązanie między obiekt1, a obiekt2. W FP robienie takich powiązań jest upierdliwe i niespecjalnie pożądane (wymaga użycia typu IO), więc się tego unika i stara przedstawić logikę przetwarzania informacji jako szereg transformacji (ze starego obiektu w nowy), a nie mutacji. Oczywiście w Javce, C#, etc też można zrezygnować z mutacji na rzecz transformacji, ale tym samym wprowadzasz (przynajmniej miejscowo) FP.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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