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, print
funkcja 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.
Ż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.
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)
a
będzie równyb
plus,c
ale nie mówi nam nica
w 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
a
dwoma pozostałymi obiektami, tak że zawsze będzie to przypadeka
równyb
plusc
. Zauważ, że umieściłem Excela na liście języków deklaratywnych, ponieważ nawet jeślib
lubc
zmieni wartość, pozostanie fakta
, 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 + c
oznacza to tylko, że przez bardzo krótki czasa
zdarzyło się równać sumieb
ic
.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 + c
informuje, że bez względu na to, co dzieje się w innym wierszu kodua
, zawsze będzie równa sumieb
ic
.źródło