Na potrzeby tej odpowiedzi definiuję „język czysto funkcjonalny” jako język funkcjonalny, w którym funkcje są referencyjnie przezroczyste, tj. Wielokrotne wywołanie tej samej funkcji z tymi samymi argumentami zawsze da takie same wyniki. Sądzę, że jest to zwykła definicja czysto funkcjonalnego języka.
Czyste funkcjonalne języki programowania nie dopuszczają efektów ubocznych (dlatego są mało przydatne w praktyce, ponieważ każdy przydatny program ma skutki uboczne, np. Gdy wchodzi w interakcję ze światem zewnętrznym).
Najłatwiejszym sposobem na osiągnięcie przejrzystości referencyjnej byłoby rzeczywiście uniemożliwienie efektów ubocznych, a tak naprawdę istnieją języki, w których tak jest (głównie te specyficzne dla danej dziedziny). Jednak z pewnością nie jest to jedyny sposób, a najbardziej funkcjonalne języki ogólnego przeznaczenia (Haskell, Clean, ...) pozwalają na efekt uboczny.
Mówienie również, że język programowania bez skutków ubocznych jest mało użyteczny w praktyce, nie jest tak naprawdę sprawiedliwe, myślę - z pewnością nie dla języków specyficznych dla domeny, ale nawet dla języków ogólnego przeznaczenia, wyobrażam sobie, że język może być całkiem użyteczny bez zapewniania efektów ubocznych . Może nie dla aplikacji konsolowych, ale myślę, że aplikacje GUI można ładnie zaimplementować bez skutków ubocznych, powiedzmy, w funkcjonalnym paradygmacie reaktywnym.
Jeśli chodzi o punkt 1, możesz wchodzić w interakcje ze środowiskiem w czysto funkcjonalnych językach, ale musisz wyraźnie oznaczyć kod (funkcje), który je wprowadza (np. W Haskell za pomocą typów monadycznych).
To nieco ponad uproszczenie. Sam system, w którym funkcje uboczne muszą być oznaczone jako takie (podobnie jak const-poprawność w C ++, ale z ogólnymi efektami ubocznymi) nie wystarczy, aby zapewnić przejrzystość referencyjną. Musisz upewnić się, że program nigdy nie może wywołać funkcji wiele razy z tymi samymi argumentami i uzyskać różne wyniki. Możesz to zrobić, tworząc takie rzeczyreadLine
być czymś, co nie jest funkcją (to właśnie robi Haskell z monadą IO) lub możesz uniemożliwić wielokrotne wywoływanie funkcji powodujących skutki uboczne przy użyciu tego samego argumentu (właśnie to robi Clean). W tym drugim przypadku kompilator zapewni, że za każdym razem, gdy wywołasz funkcję wywołującą skutki uboczne, zrobisz to z nowym argumentem i odrzuci każdy program, w którym dwukrotnie przekażesz ten sam argument funkcji wywołującej skutki uboczne.
Czyste funkcjonalne języki programowania nie pozwalają na napisanie programu, który zachowuje stan (co sprawia, że programowanie jest bardzo niewygodne, ponieważ w wielu aplikacjach potrzebujesz stanu).
Ponownie, czysto funkcjonalny język może bardzo dobrze uniemożliwiać stan zmienny, ale z pewnością można być czystym i nadal mieć stan zmienny, jeśli zastosujesz go w taki sam sposób, jak opisałem z efektami ubocznymi powyżej. Naprawdę zmienny stan to kolejna forma efektów ubocznych.
To powiedziawszy, funkcjonalne języki programowania zdecydowanie zniechęcają do zmiany stanu - szczególnie te czyste. I nie sądzę, że to sprawia, że programowanie jest niewygodne - wręcz przeciwnie. Czasami (ale nie tak często) stanu zmiennego nie da się uniknąć bez utraty wydajności lub przejrzystości (dlatego języki takie jak Haskell mają udogodnienia dla stanu zmiennego), ale najczęściej może.
Jeśli są to nieporozumienia, skąd się wzięły?
Myślę, że wiele osób po prostu czyta „funkcja musi dawać ten sam wynik, gdy wywoływana jest z tymi samymi argumentami” i wyciąga z tego wniosek, że nie jest możliwe zaimplementowanie czegoś takiego readLine
lub kodu, który zachowuje stan zmienny. Więc po prostu nie są świadomi „kodów”, których mogą używać wyłącznie funkcjonalne języki, aby wprowadzić te rzeczy bez naruszania referencyjnej przejrzystości.
Również zmienny stan mocno zniechęca w językach funkcjonalnych, więc nie jest przesadą zakładanie, że jest niedozwolony w językach funkcjonalnych.
Czy mógłbyś napisać (prawdopodobnie mały) fragment kodu ilustrujący idiomatyczny sposób Haskella do (1) implementacji efektów ubocznych i (2) implementacji obliczeń ze stanem?
Oto aplikacja w Pseudo-Haskell, która prosi użytkownika o imię i wita go. Pseudo-Haskell to język, który właśnie wymyśliłem, który ma system IO Haskella, ale używa bardziej konwencjonalnej składni, bardziej opisowych nazw funkcji i nie zawiera do
adnotacji (ponieważ to tylko odwróciłoby uwagę od tego, jak dokładnie działa monada IO):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
Wskazówka jest taka, że readLine
jest to wartość typu IO<String>
i composeMonad
jest to funkcja, która pobiera argument typu IO<T>
(dla niektórych typów T
) i kolejny argument, który jest funkcją, która pobiera argument typu T
i zwraca wartość typu IO<U>
(dla niektórych typów U
). print
jest funkcją, która pobiera ciąg znaków i zwraca wartość typu IO<void>
.
Wartość typu IO<A>
jest wartością, która „koduje” dane działanie, które generuje wartość typu A
. composeMonad(m, f)
tworzy nową IO
wartość, która koduje akcję, m
po której następuje akcja f(x)
, gdzie x
wartość jest wytwarzana przez wykonanie akcji m
.
Stan zmienny wyglądałby tak:
counter = mutableVariable(0)
increaseCounter(cnt) =
setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
composeMonad(getValue(cnt), setIncreasedValue)
printCounter(cnt) = composeMonad( getValue(cnt), print )
main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
Oto mutableVariable
funkcja, która przyjmuje wartość dowolnego typu T
i tworzy MutableVariable<T>
. Funkcja getValue
przyjmuje MutableVariable
i zwraca wartość, IO<T>
która generuje jej bieżącą wartość. setValue
przyjmuje A MutableVariable<T>
i A T
i zwraca A, IO<void>
która ustawia wartość. composeVoidMonad
jest taki sam, composeMonad
z wyjątkiem tego, że pierwszy argument jest argumentem IO
, który nie daje sensownej wartości, a drugi argument to kolejna monada, a nie funkcja, która zwraca monadę.
W Haskell jest trochę cukru syntaktycznego, który sprawia, że cała ta próba jest mniej bolesna, ale nadal jest oczywiste, że stan zmienności jest czymś, czego język tak naprawdę nie chce.