Czysty funkcjonalny kontra powiedz, nie pytaj?

14

„Idealna liczba argumentów dla funkcji wynosi zero” jest po prostu błędna. Idealna liczba argumentów to dokładnie liczba potrzebna do tego, aby twoja funkcja była wolna od efektów ubocznych. Mniej niż to, a niepotrzebnie powodujesz, że twoje funkcje są nieczyste, zmuszając cię do ucieczki od otchłani sukcesu i wspinania się po gradiencie bólu. Czasami „wujek Bob” jest na miejscu z jego radami. Czasami jest spektakularnie w błędzie. Jego rada zero argumentów jest przykładem tego ostatniego

( Źródło: komentarz @David Arno pod innym pytaniem na tej stronie )

Komentarz zyskał spektakularną liczbę 133 pozytywnych opinii, dlatego chciałbym zwrócić większą uwagę na jego treść.

O ile mi wiadomo, istnieją dwa oddzielne sposoby programowania: czyste programowanie funkcjonalne (co zachęca ten komentarz) i mów, nie pytaj (co od czasu do czasu jest również zalecane na tej stronie). AFAIK te dwie zasady są zasadniczo niekompatybilne, bliskie wzajemnym przeciwieństwom: czysta funkcjonalność może być streszczona jako „tylko zwracają wartości, nie mają skutków ubocznych”, podczas gdy powiedz, nie pytaj może być streszczona jako „nie zwracaj niczego, mają tylko skutki uboczne ”. Poza tym jestem trochę zakłopotany, ponieważ myślałem, że powiedz, nie pytaj było uważane za rdzeń paradygmatu OO, podczas gdy czyste funcitony były uważane za rdzeń paradygmatu funkcjonalnego - teraz widzę czyste funkcje zalecane w OO!

Przypuszczam, że programiści powinni wybrać jeden z tych paradygmatów i trzymać się go? Cóż, muszę przyznać, że nigdy nie mogłem zmusić się do naśladowania. Często wydaje mi się, że warto zwrócić wartość i tak naprawdę nie widzę, jak mogę osiągnąć to, co chcę osiągnąć, tylko z efektami ubocznymi. Często wydaje mi się, że mam skutki uboczne i tak naprawdę nie widzę, jak mogę osiągnąć to, co chcę osiągnąć, zwracając wartości. Często też (myślę, że to okropne) mam metody, które robią oba.

Jednak z tych 133 głosów twierdzę, że obecnie czyste programowanie funkcjonalne „wygrywa”, ponieważ staje się konsensusem, że lepiej jest powiedzieć, nie pytaj. Czy to jest poprawne?

Dlatego na przykładzie tej gry opartej na antypatternach staram się stworzyć : Gdybym chciał dostosować ją do czysto funkcjonalnego paradygmatu - JAK ?!

Wydaje mi się rozsądne mieć stan bitwy. Ponieważ jest to gra turowa, trzymam stany bitewne w słowniku (multiplayer - może być wiele bitew rozgrywanych jednocześnie przez wielu graczy). Za każdym razem, gdy gracz wykonuje swoją turę, wzywam odpowiednią metodę w stanie bitwy, która (a) odpowiednio modyfikuje stan i (b) zwraca aktualizacje graczom, które są serializowane do JSON i po prostu mówią im, co właśnie się wydarzyło na tablica. Przypuszczam, że jest to rażące naruszenie ZARÓWNO, a jednocześnie.

OK - Mógłbym uczynić metodę POWRÓT stanem bitwy zamiast modyfikować ją, jeśli naprawdę tego chcę. Ale! Czy będę musiał niepotrzebnie kopiować wszystko w stanie bitwy, aby przywrócić całkowicie nowy stan zamiast go modyfikować?

Może teraz, jeśli ruch jest atakiem, mógłbym po prostu zwrócić postacie zaktualizowane HP? Problem w tym, że nie jest to takie proste: zasady gry, ruch może i często będzie miał znacznie więcej efektów niż tylko usunięcie części HP gracza. Na przykład może zwiększyć odległość między postaciami, zastosować efekty specjalne itp.

Wydaje mi się o wiele prostsze modyfikowanie stanu i zwracanie aktualizacji ...

Ale jak doświadczony inżynier poradziłby sobie z tym?

gaazkam
źródło
9
Przestrzeganie każdego paradygmatu jest pewnym sposobem na porażkę. Polityka nigdy nie powinna przebijać inteligencji. Rozwiązanie problemu powinno zależeć od problemu, a nie od twoich przekonań religijnych na temat rozwiązywania problemów.
John Douma,
1
Nigdy wcześniej nie zadałem pytania o coś, co powiedziałem wcześniej. Jestem zaszczycony. :)
David Arno,

Odpowiedzi:

14

Jak większość aforyzmów programistycznych, „powiedz, nie proś” poświęca jasność, by zyskać zwięzłość. W ogóle nie ma na celu zalecania, aby nie pytać o wyniki obliczeń, zaleca się, aby nie pytać o dane wejściowe do obliczeń. „Nie bierz, a potem oblicz, a potem ustaw, ale dobrze jest zwrócić wartość z obliczeń”, nie jest tak zwięzłe.

Kiedyś ludzie często dzwonili do gettera, robili na nim obliczenia, a następnie dzwonili do setera z wynikiem. Jest to wyraźny znak, że twoje obliczenia faktycznie należą do klasy, w której nazywasz getter. Zostało wymyślone „powiedz, nie pytaj”, aby przypominać ludziom, aby zwracali uwagę na ten anty-wzór i działało tak dobrze, że teraz niektórzy uważają, że ta część jest oczywista, i szukają innych rodzajów „prosi” wyeliminować. Jednak aforyzm jest użyteczny tylko w tej jednej sytuacji.

Czyste programy funkcjonalne nigdy nie cierpiały z powodu dokładnie tego anty-wzoru, z tego prostego powodu, że nie ma seterów w tym stylu. Jednak bardziej ogólny (i trudniejszy do zauważenia) problem nie mieszania różnych poziomów abstrakcji semantycznej w tej samej funkcji dotyczy każdego paradygmatu.

Karl Bielefeldt
źródło
Dziękujemy za prawidłowe wyjaśnienie „Powiedz, nie pytaj”.
user949300,
13

Zarówno wujek Bob, jak i David Arno (autor cytatu) mieli ważne lekcje, które możemy wyciągnąć z tego, co napisali. Myślę, że warto nauczyć się tej lekcji, a następnie ekstrapolować, co to tak naprawdę oznacza dla ciebie i twojego projektu.

Po pierwsze: lekcja wuja Boba

Wujek Bob podkreśla, że ​​im więcej argumentów masz w swojej funkcji / metodzie, tym więcej programistów, którzy jej używają, musi zrozumieć. To obciążenie poznawcze nie przychodzi za darmo, a jeśli nie jesteś zgodny z kolejnością argumentów itp., Obciążenie poznawcze tylko wzrasta.

To fakt bycia człowiekiem. Myślę, że kluczowym błędem w książce „Czysty kod wuja Boba jest stwierdzenie „Idealna liczba argumentów dla funkcji wynosi zero” . Minimalizm jest świetny, dopóki nie będzie. Tak jak nigdy nie osiągasz swoich limitów w rachunku, nigdy nie osiągniesz „idealnego” kodu - i nie powinieneś.

Jak powiedział Albert Einstein: „Wszystko powinno być tak proste, jak to tylko możliwe, ale nie prostsze”.

Po drugie: Lekcja Davida Arno

Opisany sposób opracowania Davida Arno to bardziej funkcjonalny rozwój stylu niż zorientowanie obiektowe . Jednak kod funkcjonalny skaluje się znacznie lepiej niż tradycyjne programowanie obiektowe. Dlaczego? Z powodu blokowania. Za każdym razem, gdy stan jest zmienny w obiekcie, ryzykujesz warunki wyścigu lub rywalizację o blokadę.

Po napisaniu wysoce współbieżnych systemów wykorzystywanych w symulacjach i innych aplikacjach po stronie serwera, model funkcjonalny działa cuda. Mogę zaświadczyć o wprowadzonych ulepszeniach. Jest to jednak zupełnie inny styl rozwoju, z różnymi wymaganiami i idiomami.

Rozwój to szereg kompromisów

Znasz swoją aplikację lepiej niż ktokolwiek z nas. Skalowalność dostarczana wraz z funkcjonalnym programowaniem może nie być potrzebna. Między dwoma wymienionymi wyżej ideałami istnieje świat. Ci z nas, którzy mają do czynienia z systemami, które muszą obsługiwać wysoką przepustowość i śmieszny paralelizm, będą dążyć do ideału programowania funkcjonalnego.

To powiedziawszy, możesz użyć obiektów danych do przechowywania zestawu informacji, które musisz przekazać metodzie. Pomaga to w rozwiązaniu problemu obciążenia poznawczego, którym zajmował się wujek Bob, jednocześnie wspierając funkcjonalny ideał, którym zajmował się David Arno.

Pracowałem na obu komputerach stacjonarnych z wymaganym ograniczeniem równoległości i oprogramowaniem do symulacji o dużej przepustowości. Mają bardzo różne potrzeby. Potrafię docenić dobrze napisany obiektowy kod, który został zaprojektowany wokół koncepcji ukrywania danych, którą znasz. Działa dla kilku aplikacji. Jednak nie działa na wszystkie z nich.

Kto ma rację W tym przypadku David ma rację bardziej niż wujek Bob. Jednak zasadniczą kwestią, którą chcę tutaj podkreślić, jest to, że metoda powinna mieć tyle argumentów, ile ma sens.

Berin Loritsch
źródło
Istnieje paralelizm. Różne bitwy mogą być przetwarzane równolegle. Jednak tak: pojedyncza bitwa podczas przetwarzania musi zostać zablokowana.
gaazkam
Tak, miałem na myśli, że czytelnicy (żniwiarze w twojej analogii) będą czerpać z ich (siewcy) pism. To powiedziawszy, wróciłem, aby spojrzeć na niektóre rzeczy, które napisałem w przeszłości i albo coś się nauczyłem, albo nie zgodziłem się z moim poprzednim sobą. Wszyscy się uczymy i rozwijamy, i to jest powód numer jeden, dlatego zawsze powinieneś rozumować, jak i jeśli zastosujesz coś, czego się nauczyłeś.
Berin Loritsch
8

OK - Mógłbym uczynić metodę POWRÓT stanem bitwy zamiast modyfikować ją, jeśli naprawdę tego chcę.

Tak, to jest pomysł.

Czy będę musiał skopiować wszystko w stanie bitwy, aby przywrócić całkowicie nowy stan zamiast go modyfikować?

Nie. Twój „stan bitwy” może być modelowany jako niezmienna struktura danych, która zawiera inne niezmienne struktury danych jako bloki konstrukcyjne, być może zagnieżdżone w niektórych hierarchiach niezmiennych struktur danych.

Mogą więc istnieć części stanu bitwy, których nie trzeba zmieniać podczas jednej tury, i inne, które należy zmienić. Części, które się nie zmieniają, nie muszą być kopiowane, ponieważ są niezmienne, wystarczy skopiować odniesienie do tych części, bez ryzyka wprowadzenia efektów ubocznych. Działa to najlepiej w środowiskach językowych zbierających śmieci.

Google za „Efektywne niezmienne struktury danych”, a na pewno znajdziesz kilka referencji, jak to ogólnie działa.

Wydaje mi się o wiele prostsze modyfikowanie stanu i zwracanie aktualizacji.

W przypadku niektórych problemów może to być rzeczywiście prostsze. Gry i symulacje oparte na rundach mogą należeć do tej kategorii, biorąc pod uwagę dużą część zmian stanu gry z jednej rundy do drugiej. Jednak postrzeganie tego, co jest naprawdę „prostsze”, jest do pewnego stopnia subiektywne i zależy również od tego, do czego ludzie są przyzwyczajeni.

Doktor Brown
źródło
8

Jako autor komentarza, chyba powinienem to wyjaśnić, ponieważ oczywiście jest w tym coś więcej niż uproszczona wersja, którą oferuje mój komentarz.

AFAIK te dwie zasady są zasadniczo niezgodne, bliskie wzajemnym przeciwieństwom: czysta funkcjonalność może być streszczona jako „tylko zwracają wartości, nie mają skutków ubocznych”, podczas gdy mów, nie pytaj może być streszczona jako „nie zwracaj niczego, mają tylko skutki uboczne ”.

Szczerze mówiąc, uważam to za bardzo dziwne użycie terminu „mów, nie pytaj”. Przeczytałem więc kilka słów temu Martin Fowler kilka lat temu, co było pouczające . Powodem, dla którego uznałem to za dziwne, jest to, że „powiedz, nie pytaj” jest synonimem wstrzykiwania zależności w mojej głowie, a najczystszą formą wstrzykiwania zależności jest przekazywanie wszystkiego, czego potrzebuje funkcja za pomocą jej parametrów.

Wygląda jednak na to, że znaczenie, które stosuję do „powiedz, nie pytaj” pochodzi z przyjęcia definicji Fowlera skoncentrowanej na OO i uczynienia go bardziej agnostycznym. Uważam, że proces ten doprowadza koncept do logicznych wniosków.

Wróćmy do prostych początków. Mamy „bryły logiki” (procedury) i dane globalne. Procedury odczytują te dane bezpośrednio, aby uzyskać do nich dostęp. Mamy prosty scenariusz „zapytaj”.

Lekko przewiń do przodu. Mamy teraz obiekty i metody. Te dane nie muszą już być globalne, mogą być przekazywane przez konstruktor i zawarte w obiekcie. A potem mamy metody, które działają na podstawie tych danych. Teraz mamy „powiedz, nie pytaj”, jak to opisuje Fowler. Obiekt jest informowany o swoich danych. Metody te nie muszą już pytać globalnego zakresu o swoje dane. Ale tutaj jest rub: to wciąż nie jest prawda „powiedz, nie pytaj” moim zdaniem, ponieważ te metody wciąż muszą pytać o zakres obiektu. To bardziej scenariusz „powiedz, a potem zapytaj”.

Wróć więc do czasów współczesnych, porzuć podejście „wszystko jest w porządku” i zapożycz kilka zasad z programowania funkcjonalnego. Teraz, gdy wywoływana jest metoda, wszystkie dane są do niej dostarczane za pośrednictwem jej parametrów. Można (i już) argumentowano: „o co chodzi, to tylko komplikuje kod?” I tak, przekazywanie za pomocą parametrów, danych, które są dostępne przez zakres obiektu, powoduje, że kod jest bardziej skomplikowany. Ale przechowywanie tych danych w obiekcie, zamiast uczynienia go globalnie dostępnym, również zwiększa złożoność. Jednak niewielu twierdzi, że zmienne globalne są zawsze lepsze, ponieważ są prostsze. Chodzi o to, że korzyści, które przynosi „mów, nie pytaj”, przewyższają złożoność ograniczania zakresu. Dotyczy to bardziej przekazywania parametrów niż ograniczania zakresu do obiektu.private statici przekazać wszystko, czego potrzebuje za pomocą parametrów, a teraz tej metodzie można zaufać, aby niejawnie uzyskiwać dostęp do rzeczy, których nie powinna. Ponadto zachęca do utrzymywania małej metody, w przeciwnym razie lista parametrów wymknie się spod kontroli. I zachęca do pisania metod, które pasują do kryteriów „czystej funkcji”.

Nie widzę więc „czysto funkcjonalnych” i „powiedz, nie pytaj” jako przeciwnych do siebie. Pierwsza jest jedyną pełną implementacją drugiej, jeśli o mnie chodzi. Podejście Fowlera nie jest kompletne „powiedz, nie pytaj”.

Ale ważne jest, aby pamiętać, że ta „pełna realizacja polecenia nie pytaj” naprawdę jest ideałem, tzn. Pragmatyzm musi się pojawiać mniej, gdy stajemy się idealistyczni, a tym samym źle traktujemy go jako jedyne możliwie właściwe podejście. Bardzo niewiele aplikacji może zbliżyć się do bycia w 100% bez skutków ubocznych z tego prostego powodu, że nie zrobiłyby nic pożytecznego, gdyby były naprawdę wolne od skutków ubocznych. Potrzebujemy zmiany stanu, potrzebujemy IO itp., Aby aplikacja była przydatna. W takich przypadkach metody muszą powodować działania niepożądane i dlatego nie mogą być czyste. Ale podstawową zasadą jest ograniczenie tych „nieczystych” metod do minimum; mają tylko skutki uboczne, ponieważ muszą, a nie jak normę.

Wydaje mi się rozsądne mieć stan bitwy. Ponieważ jest to gra turowa, trzymam stany bitewne w słowniku (multiplayer - może być wiele bitew rozgrywanych jednocześnie przez wielu graczy). Za każdym razem, gdy gracz wykonuje swoją turę, wzywam odpowiednią metodę w stanie bitwy, która (a) odpowiednio modyfikuje stan i (b) zwraca aktualizacje graczom, które są serializowane do JSON i po prostu mówią im, co właśnie się wydarzyło na tablica.

Wydaje mi się bardziej niż rozsądne mieć stan wojenny; wydaje się to niezbędne. Głównym celem takiego kodu jest obsługa wniosków o zmianę stanu, zarządzanie tymi zmianami stanu i zgłaszanie ich z powrotem. Możesz poradzić sobie z tym stanem na całym świecie, możesz trzymać go w obiektach poszczególnych graczy lub przekazać go wokół zestawu czystych funkcji. Który wybierzesz sprowadza się do tego, który działa najlepiej w danym scenariuszu. Stan globalny upraszcza projektowanie kodu i jest szybki, co jest kluczowym wymogiem większości gier. Ale sprawia, że ​​kod jest trudniejszy w utrzymaniu, testowaniu i debugowaniu. Zestaw czystych funkcji sprawi, że kod będzie bardziej skomplikowany do wdrożenia i ryzykuje, że będzie zbyt wolny z powodu nadmiernego kopiowania danych. Ale najłatwiej będzie to przetestować i utrzymać. „Podejście OO” znajduje się w połowie drogi.

Kluczem jest: nie ma jednego idealnego rozwiązania, które działałoby przez cały czas. Celem czystych funkcji jest pomóc ci „wpaść w pułapkę sukcesu”. Ale jeśli ta dziura jest tak płytka, ze względu na złożoność, jaką może przynieść kodowi, że nie wpadasz w nią tak bardzo, jak potknięcie się o nią, to nie jest to odpowiednie podejście. Celuj w ideał, ale bądź pragmatyczny i przestań, gdy ten ideał nie jest tym razem dobrym miejscem.

I na koniec, żeby powtórzyć: czyste funkcje i „mów, nie pytaj” wcale nie są przeciwieństwami.

David Arno
źródło
5

Cokolwiek, co kiedykolwiek powiedziano, istnieje kontekst, w którym można umieścić to stwierdzenie, co sprawi, że będzie to absurdalne.

wprowadź opis zdjęcia tutaj

Wujek Bob jest całkowicie w błędzie, jeśli weźmiesz za zalecenie zerowej argumentacji. Ma całkowitą rację, jeśli weźmiesz to pod uwagę, że każdy dodatkowy argument utrudnia odczytanie kodu. To kosztuje. Nie dodajesz argumentów do funkcji, ponieważ ułatwia to ich odczytanie. Dodajesz argumenty do funkcji, ponieważ nie możesz wymyślić dobrego imienia, które uwidacznia zależność od tego argumentu.

Na przykład pi()jest to doskonale dobra funkcja. Dlaczego? Ponieważ nie obchodzi mnie, jak, a nawet czy to zostało obliczone. Lub jeśli użył e lub sin (), aby dojść do liczby, którą zwraca. Nic mi nie jest, ponieważ nazwa mówi mi wszystko, co muszę wiedzieć.

Jednak nie każde imię mówi mi wszystko, co muszę wiedzieć. Niektóre nazwy nie ujawniają ważnych informacji, które kontrolują zachowanie funkcji, a także ujawniane argumenty. Właśnie to sprawia, że ​​funkcjonalny styl programowania jest łatwiejszy do uzasadnienia.

Mogę zachować niezmienne i wolne od efektów ubocznych w stylu całkowicie OOP. Zwrot jest po prostu mechaniką używaną do pozostawienia wartości na stosie do następnej procedury. Możesz pozostać tak samo niezmienny, używając portów wyjściowych do przekazywania wartości innym niezmiennym rzeczom, dopóki nie trafisz na ostatni port wyjściowy, który w końcu musi coś zmienić, jeśli chcesz, aby ludzie mogli go odczytać. Dotyczy to każdego języka, funkcjonalnego lub nie.

Nie twierdzę więc, że programowanie funkcjonalne i programowanie obiektowe są „zasadniczo niezgodne”. Mogę używać obiektów w moich programach funkcjonalnych i mogę używać czystych funkcji w moich programach OO.

Ich mieszanie wiąże się jednak z pewnymi kosztami: oczekiwania. Możesz wiernie przestrzegać zasad obu paradygmatów i nadal powodować zamieszanie. Jedną z zalet używania funkcjonalnego języka jest to, że efekty uboczne, mimo że muszą istnieć, aby uzyskać jakikolwiek efekt, są umieszczane w przewidywalnym miejscu. O ile oczywiście do przedmiotu zmiennego nie można uzyskać dostępu w niezdyscyplinowany sposób. To, co podałeś w danym języku, rozpada się.

Podobnie możesz obsługiwać obiekty o czystych funkcjach, możesz projektować obiekty, które są niezmienne. Problem polega na tym, że jeśli nie zasygnalizujesz, że funkcje są czyste lub obiekty są niezmienne, ludzie nie skorzystają z tych funkcji, dopóki nie spędzą dużo czasu na czytaniu kodu.

To nie jest nowy problem. Przez lata ludzie kodowali proceduralnie w „językach OO”, myśląc, że robią OO, ponieważ używają „języka OO”. Niewiele języków jest tak dobrych w powstrzymywaniu cię od strzelania sobie w stopę. Aby te pomysły zadziałały, muszą żyć w tobie.

Oba zapewniają dobre funkcje. Możesz zrobić jedno i drugie. Jeśli masz dość odwagi, by je wymieszać, opisz je wyraźnie.

candied_orange
źródło
0

Czasami próbuję zrozumieć wszystkie zasady różnych paradygmatów. Czasami są ze sobą w sprzeczności, ponieważ znajdują się w takiej sytuacji.

OOP to imperatywny paradygmat polegający na bieganiu nożyczkami w świecie, w którym zdarzają się niebezpieczne rzeczy.

FP jest paradygmatem funkcjonalnym, w którym absolutne bezpieczeństwo znajduje się w czystych obliczeniach. Nic się tu nie dzieje.

Jednak wszystkie programy muszą przejść do imperatywnego świata, aby były przydatne. Tak więc funkcjonalny rdzeń, imperatywna powłoka .

Sprawy stają się mylące, gdy zaczynasz definiować niezmienne obiekty (te, których polecenia zwracają zmodyfikowaną kopię, a nie mutację). Mówicie sobie: „To jest OOP” i „Definiuję zachowanie obiektu”. Wracasz do sprawdzonej zasady Tell, Don't Ask. Problem w tym, że stosujesz to w niewłaściwym królestwie.

Kraje są zupełnie inne i podlegają różnym zasadom. Sfera funkcjonalna buduje się do tego stopnia, że ​​chce uwolnić efekty uboczne na świecie. Aby te efekty zostały uwolnione, wszystkie dane, które zostałyby zawarte w obiekcie imperatywnym (gdyby to zostało zapisane w ten sposób!), Muszą być dostępne w trybie powłoki imperatywnej. Bez dostępu do tych danych, które w innym świecie zostałyby ukryte przez enkapsulację, nie może wykonać pracy. Jest to obliczeniowo niemożliwe.

Tak więc, kiedy piszesz niezmienne obiekty (które Clojure nazywa trwałymi strukturami danych) pamiętaj, że jesteś w domenie funkcjonalnej. Rzuć powiedz, nie pytaj przez okno i wpuść je z powrotem do domu tylko wtedy, gdy ponownie wejdziesz do imperatywnego królestwa.

Mario T. Lanza
źródło