Jeśli funkcjonalne języki programowania nie mogą zapisać żadnego stanu, w jaki sposób robią proste rzeczy, takie jak odczytywanie danych wejściowych od użytkownika (mam na myśli, jak je „przechowują”) lub przechowywanie jakichkolwiek danych w tym zakresie?
Jak już wiesz, programowanie funkcjonalne nie ma stanu - ale to nie znaczy, że nie może przechowywać danych. Różnica polega na tym, że jeśli napiszę oświadczenie (Haskell) w stylu
let x = func value 3.14 20 "random"
in ...
Mam gwarancję, że wartość x
jest zawsze taka sama w ...
: nic nie może tego zmienić. Podobnie, jeśli mam funkcję f :: String -> Integer
(funkcję pobierającą ciąg znaków i zwracającą liczbę całkowitą), mogę być pewien, że f
nie zmodyfikuje ona swojego argumentu, nie zmieni żadnych zmiennych globalnych, nie zapisze danych do pliku i tak dalej. Jak powiedział sepp2k w komentarzu powyżej, ta niezmienność jest naprawdę pomocna w rozumowaniu o programach: piszesz funkcje, które zwijają, zwijają i niszczą twoje dane, zwracając nowe kopie, abyś mógł je połączyć w łańcuch, i możesz być pewien, że żadne z tych wywołań funkcji może zrobić wszystko, co jest „szkodliwe”. Wiesz, że tak x
jest zawsze x
i nie musisz się martwić, że ktoś napisał x := foo bar
gdzieś pomiędzy deklaracjąx
i jego zastosowanie, bo to niemożliwe.
A co, jeśli chcę odczytać dane wejściowe od użytkownika? Jak powiedział KennyTM, chodzi o to, że nieczysta funkcja jest czystą funkcją, która jako argument przekazuje całemu światu i zwraca zarówno wynik, jak i świat. Oczywiście nie chcesz tego robić: po pierwsze, jest to okropnie niezgrabne, a po drugie, co się stanie, jeśli ponownie wykorzystam ten sam obiekt świata? Więc to jest w jakiś sposób abstrakcyjne. Haskell obsługuje to z typem IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
To mówi nam, że main
jest to akcja IO, która nic nie zwraca; wykonanie tej czynności oznacza uruchomienie programu Haskell. Zasada jest taka, że typy we / wy nigdy nie mogą uciec od akcji we / wy; w tym kontekście wprowadzamy tę akcję za pomocą do
. W ten sposób getLine
zwraca an IO String
, co można sobie wyobrazić na dwa sposoby: po pierwsze, jako działanie, które po uruchomieniu tworzy napis; po drugie, jako ciąg, który jest „skażony” przez IO, ponieważ został otrzymany w sposób nieczysty. Pierwsza jest bardziej poprawna, ale druga może być bardziej pomocna. <-
Bierze String
OUT IO String
i przechowuje je w str
-ale skoro jesteśmy w działaniu IO, musimy owinąć go wykonać kopię zapasową, więc to nie może „uciec”. Następny wiersz próbuje odczytać liczbę całkowitą ( reads
) i pobiera pierwsze udane dopasowanie (fst . head
); to wszystko jest czyste (bez IO), więc nadajemy mu nazwę let no = ...
. Możemy wtedy użyć obu no
i str
w ...
. W ten sposób przechowujemy nieczyste dane (z getLine
do str
) i czyste dane ( let no = ...
).
Ten mechanizm pracy z IO jest bardzo potężny: pozwala oddzielić czystą, algorytmiczną część programu od nieczystej strony interakcji użytkownika i wymusić to na poziomie typu. Twoja minimumSpanningTree
funkcja prawdopodobnie nie może zmienić czegoś w innym miejscu w kodzie, napisać wiadomości do użytkownika i tak dalej. To jest bezpieczne.
To wszystko, co musisz wiedzieć, aby używać IO w Haskell; jeśli to wszystko, czego chcesz, możesz zatrzymać się tutaj. Ale jeśli chcesz zrozumieć, dlaczego to działa, czytaj dalej. (Pamiętaj, że te rzeczy będą specyficzne dla Haskella - inne języki mogą wybrać inną implementację).
Więc to prawdopodobnie wydawało się trochę oszustwem, w jakiś sposób dodając nieczystości do czystego Haskella. Ale tak nie jest - okazuje się, że możemy zaimplementować typ IO całkowicie w czystym Haskellu (o ile otrzymamy RealWorld
). Idea jest taka: akcja IO IO type
jest tym samym, co funkcja RealWorld -> (type, RealWorld)
, która przyjmuje świat rzeczywisty i zwraca zarówno obiekt typu, jak type
i zmodyfikowany RealWorld
. Następnie definiujemy kilka funkcji, abyśmy mogli używać tego typu bez szaleństwa:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
Pierwsza z nich pozwala nam mówić o akcjach IO, które nic nie robią: return 3
jest to akcja IO, która nie pyta o rzeczywisty świat i po prostu zwraca 3
. >>=
Operator, wymawiane „bind”, pozwoli nam uruchomić działania IO. Wyodrębnia wartość z akcji IO, przekazuje ją i świat rzeczywisty przez funkcję i zwraca wynikową akcję IO. Zauważ, że >>=
wymusza naszą zasadę, że wyniki działań IO nigdy nie mogą uciec.
Możemy następnie przekształcić powyższe main
w następujący zwykły zestaw aplikacji funkcji:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Skok środowiska main
uruchomieniowego Haskell zaczyna się od inicjału RealWorld
i gotowe! Wszystko jest czyste, ma po prostu fantazyjną składnię.
[ Edycja: Jak wskazuje @Conal , nie jest to właściwie to, czego Haskell używa do wykonania IO. Ten model zepsuje się, jeśli dodasz współbieżność lub rzeczywiście w jakikolwiek sposób, aby świat zmienił się w środku akcji IO, więc Haskell nie mógłby użyć tego modelu. Jest dokładna tylko dla obliczeń sekwencyjnych. Dlatego może się zdarzyć, że IO Haskella jest trochę uniku; nawet jeśli nie jest, z pewnością nie jest aż tak elegancki. Obserwacja Per @ Conal, zobacz, co mówi Simon Peyton-Jones w Tackling the Awkward Squad [pdf] , sekcja 3.1; przedstawia coś, co mogłoby oznaczać alternatywny model wzdłuż tych linii, ale potem porzuca go ze względu na jego złożoność i przyjmuje inną taktykę.]
Ponownie, to wyjaśnia (prawie), jak IO i ogólnie zmienność działają w Haskell; jeśli to wszystko, co chcesz wiedzieć, możesz przestać czytać tutaj. Jeśli potrzebujesz ostatniej dawki teorii, czytaj dalej - ale pamiętaj, że w tym momencie odeszliśmy naprawdę daleko od twojego pytania!
A więc ostatnia rzecz: okazuje się, że ta struktura - typ parametryczny z return
i >>=
- jest bardzo ogólna; Nazywa się to monadą i do
zapisem return
i >>=
działa z każdym z nich. Jak widziałeś tutaj, monady nie są magiczne; magiczne jest to, że do
bloki zamieniają się w wywołania funkcji. RealWorld
Typ jest jedynym miejscem widzimy żadnej magii. Typy takie jak []
, konstruktor list, są również monadami i nie mają nic wspólnego z nieczystym kodem.
Wiesz już (prawie) wszystko o pojęciu monady (poza kilkoma prawami, które muszą być spełnione i formalną definicją matematyczną), ale brakuje ci intuicji. W Internecie jest absurdalna liczba samouczków monad; Podoba mi się ten , ale masz opcje. Jednak to prawdopodobnie nie pomoże ; jedynym prawdziwym sposobem na uzyskanie intuicji jest połączenie ich używania i przeczytania kilku samouczków we właściwym czasie.
Jednak nie potrzebujesz tej intuicji, aby zrozumieć IO . Zrozumienie monad w całości jest wisienką na torcie, ale możesz już teraz użyć IO. Możesz go użyć po tym, jak pokazałem ci pierwszą main
funkcję. Możesz nawet traktować kod IO tak, jakby był w nieczystym języku! Pamiętaj jednak, że istnieje podstawowa reprezentacja funkcjonalna: nikt nie oszukuje.
(PS: Przepraszam za długość. Poszedłem trochę daleko.)