Czy funkcjonalne języki programowania zapobiegają skutkom ubocznym?

10

Według Wikipedii funkcjonalne języki programowania , które są deklaratywne, nie pozwalają na skutki uboczne. Programowanie deklaratywne w ogólności, próbuje zminimalizować lub wyeliminować skutki uboczne.

Ponadto, zgodnie z Wikipedią, efekt uboczny jest związany ze zmianami stanu. Tak więc funkcjonalne języki programowania w tym sensie faktycznie eliminują skutki uboczne, ponieważ nie zapisują żadnego stanu.

Ale dodatkowo efekt uboczny ma inną definicję. Efekt uboczny

ma zauważalną interakcję z funkcjami wywoływania lub światem zewnętrznym, oprócz zwracania wartości. Na przykład określona funkcja może modyfikować zmienną globalną lub zmienną statyczną, modyfikować jeden z jej argumentów, zgłaszać wyjątek, zapisywać dane na wyświetlaczu lub w pliku, odczytywać dane lub wywoływać inne funkcje niepożądane.

W tym sensie funkcjonalne języki programowania faktycznie dopuszczają skutki uboczne, ponieważ istnieją niezliczone przykłady funkcji wpływających na ich świat zewnętrzny, wywoływanie innych funkcji, zgłaszanie wyjątków, pisanie w plikach itp.

Wreszcie, czy funkcjonalne języki programowania dopuszczają skutki uboczne?

Lub też nie rozumiem, co kwalifikuje się jako „efekt uboczny”, więc języki imperatywne pozwalają na to, a deklaratywne nie. Zgodnie z powyższym i tym, co otrzymuję, żaden język nie eliminuje skutków ubocznych, więc albo brakuje mi czegoś o skutkach ubocznych, albo definicja Wikipedii jest niepoprawnie szeroka.

codebot
źródło

Odpowiedzi:

26

Programowanie funkcjonalne obejmuje wiele różnych technik. Niektóre techniki są w porządku z efektami ubocznymi. Ale jednym ważnym aspektem jest rozumowanie równań : jeśli wywołam funkcję o tej samej wartości, zawsze otrzymam ten sam wynik. Mogę więc zastąpić wywołanie funkcji wartością zwracaną i uzyskać równoważne zachowanie. Ułatwia to rozumowanie programu, zwłaszcza podczas debugowania.

Jeśli funkcja ma skutki uboczne, nie do końca to działa. Zwracana wartość nie jest równoważna wywołaniu funkcji, ponieważ zwracana wartość nie zawiera skutków ubocznych.

Rozwiązanie polega na wyłączeniu niepożądane efekty, kodującego te efekty w wartości powrotnej . Różne języki mają różne systemy efektów. Np. Haskell używa monad do kodowania niektórych efektów, takich jak IO lub mutacja stanu. Języki C / C ++ / Rust mają system typów, który może uniemożliwić mutację niektórych wartości.

W języku imperatywnym print("foo")funkcja wydrukuje coś i niczego nie zwróci. W czysto funkcjonalnym języku, takim jak Haskell, printfunkcja przyjmuje również obiekt reprezentujący stan świata zewnętrznego i zwraca nowy obiekt reprezentujący stan po wykonaniu tego wyniku. Coś podobnego do newState = print "foo" oldState. Mogę utworzyć tyle nowych stanów ze starego stanu, ile mi się podoba. Jednak tylko jedna będzie kiedykolwiek używana przez funkcję główną. Muszę więc zsekwencjonować stany z wielu akcji, łącząc łańcuchy funkcji. Aby wydrukować foo bar, mógłbym powiedzieć coś takiego print "bar" (print "foo" originalState).

Jeśli stan wyjściowy nie jest używany, Haskell nie wykonuje działań prowadzących do tego stanu, ponieważ jest to leniwy język. I odwrotnie, to lenistwo jest możliwe tylko dlatego, że wszystkie efekty są wyraźnie zakodowane jako wartości zwracane.

Pamiętaj, że Haskell jest jedynym powszechnie używanym językiem funkcjonalnym, który korzysta z tej trasy. Inne języki funkcjonalne, w tym rodzina Lisp, rodzina ML i nowsze języki funkcjonalne, takie jak Scala, zniechęcają, ale dopuszczają wciąż skutki uboczne - można je nazwać językami imperatywno-funkcjonalnymi.

Korzystanie ze skutków ubocznych we / wy jest prawdopodobnie w porządku. Często operacje we / wy (inne niż logowanie) są wykonywane tylko na zewnętrznej granicy systemu. W logice biznesowej nie zachodzi żadna komunikacja zewnętrzna. Następnie można napisać rdzeń oprogramowania w czystym stylu, jednocześnie wykonując nieczyste operacje we / wy w zewnętrznej powłoce. Oznacza to również, że rdzeń może być bezpaństwowy.

Bezpaństwowość ma wiele praktycznych zalet, takich jak zwiększona racjonalność i skalowalność. Jest to bardzo popularne w backendach aplikacji internetowych. Każdy stan jest przechowywany na zewnątrz, we wspólnej bazie danych. Ułatwia to równoważenie obciążenia: nie muszę przyklejać sesji do konkretnego serwera. Co jeśli potrzebuję więcej serwerów? Po prostu dodaj kolejny, ponieważ używa tej samej bazy danych. Co się stanie, jeśli jeden serwer ulegnie awarii? Mogę ponowić wszelkie oczekujące żądania na innym serwerze. Oczywiście nadal istnieje stan - w bazie danych. Ale wyraziłem to jasno i wyodrębniłem, i jeśli mogę, mogę zastosować podejście czysto funkcjonalne wewnętrznie.

amon
źródło
Dziękuję za szczegółową odpowiedź. Jako wnioski uważam, że skutki uboczne nie wpływają na wartość funkcji, ze względu na rozumowanie równań, dlatego „języki funkcjonalne nie pozwalają / minimalizują skutków ubocznych”. Efekty osadzone w wartościach funkcji wpływają i zmieniają stan, który kiedykolwiek został zapisany - lub zapisany poza rdzeniem programu. Ponadto operacje we / wy mają miejsce na zewnętrznej granicy logiki biznesowej.
codebot
3
@codebot, Moim zdaniem niezupełnie. Przy prawidłowym wdrożeniu skutki uboczne w programowaniu funkcjonalnym powinny znaleźć odzwierciedlenie w typie zwracanej funkcji. Na przykład, jeśli funkcja może się nie powieść (jeśli nie istnieje określony plik lub nie można nawiązać połączenia z bazą danych), wówczas typ zwracany przez funkcję powinien zawierać błąd, a nie zgłaszać wyjątek. Na przykład spójrz na programowanie zorientowane na kolej ( fsharpforfunandprofit.com/posts/recipe-part2 ).
Aaron M. Eshbach,
„... można je nazwać imperatywnymi-funkcjonalnymi językami.”: Simon Peyton Jones napisał „… Haskell jest najlepszym na świecie imperatywnym językiem programowania”.
Giorgio
5

Żaden język programowania nie eliminuje skutków ubocznych. Myślę, że lepiej jest powiedzieć, że języki deklaratywne zawierają skutki uboczne, podczas gdy języki imperatywne nie. Nie jestem jednak pewien, czy którykolwiek z tych rozmów na temat skutków ubocznych ma zasadniczą różnicę między tymi dwoma rodzajami języków i wydaje się, że tak naprawdę to, czego szukasz.

Myślę, że to pomaga zilustrować różnicę na przykładzie.

a = b + c

Powyższy wiersz kodu można napisać w praktycznie dowolnym języku, więc w jaki sposób możemy ustalić, czy używamy języka rozkazującego, czy deklaratywnego? Czym różnią się właściwości tego wiersza kodu w dwóch klasach języka?

W języku imperatywnym (C, Java, JavaScript i c.) Ten wiersz kodu stanowi jedynie krok w procesie. Nie mówi nam nic o podstawowej naturze którejkolwiek z wartości. Mówi nam, że w tej chwili po tym wierszu kodu (ale przed następnym wierszem) abędzie równy bplus, cale nie mówi nam nic aw szerszym tego słowa znaczeniu.

W języku deklaratywnym (Haskell, Scheme, Excel i c.) Ten wiersz kodu mówi o wiele więcej. Ustanawia niezmienną relację między adwoma pozostałymi obiektami, tak że zawsze będzie to przypadek arówny bplus c. Zauważ, że umieściłem Excela na liście języków deklaratywnych, ponieważ nawet jeśli blub czmieni wartość, pozostanie fakt a, który będzie równy ich sumie.

Moim zdaniem to nie skutki uboczne czy stan są tym, co wyróżnia oba typy języków. W języku imperatywnym jakikolwiek konkretny wiersz kodu nic nie mówi o ogólnym znaczeniu danych zmiennych. Innymi słowy, a = b + coznacza to tylko, że przez bardzo krótki czas azdarzyło się równać sumie bi c.

Tymczasem w językach deklaratywnych każdy wiersz kodu ustanawia podstawową prawdę, która będzie istnieć przez cały okres istnienia programu. W tych językach a = b + cinformuje, że bez względu na to, co dzieje się w innym wierszu kodu a, zawsze będzie równa sumie bi c.

Daniel T.
źródło