Widoki 1 i 2 są ogólnie niepoprawne.
- Każdy rodzaj danych
* -> *
może działać jako etykieta, monady to znacznie więcej.
- (Z wyjątkiem
IO
monady) obliczenia w monadzie nie są nieczyste. Po prostu reprezentują obliczenia, które postrzegamy jako mające skutki uboczne, ale są czyste.
Oba te nieporozumienia wynikają z koncentracji na IO
monadzie, która w rzeczywistości jest trochę wyjątkowa.
Spróbuję trochę rozwinąć # 3, nie wchodząc w teorię kategorii, jeśli to możliwe.
Standardowe obliczenia
Wszystkie obliczenia w funkcjonalnego języka programowania może być postrzegana jako funkcje z typem źródłowego i docelowego typu: f :: a -> b
. Jeśli funkcja ma więcej niż jeden argument, możemy przekonwertować ją na funkcję jednego argumentu przez curry (patrz także wiki Haskell ). A jeśli mamy tylko wartości x :: a
(0 funkcji z argumentami), możemy przekształcić go w funkcję, która pobiera argument typu urządzenia : (\_ -> x) :: () -> a
.
Możemy budować bardziej złożone programy z prostszych, tworząc takie funkcje za pomocą .
operatora. Na przykład, jeśli mamy f :: a -> b
i g :: b -> c
dostaniemy g . f :: a -> c
. Zauważ, że działa to również w przypadku naszych przeliczonych wartości: jeśli mamy x :: a
i przekonwertujemy to na naszą reprezentację, otrzymamy f . ((\_ -> x) :: () -> a) :: () -> b
.
Ta reprezentacja ma kilka bardzo ważnych właściwości, a mianowicie:
- Mamy bardzo specjalny Funkcja - tożsamość funkcję
id :: a -> a
dla każdego typu a
. Jest to element tożsamości w odniesieniu do .
: f
jest równy f . id
i do id . f
.
- Operator kompozycji funkcji
.
jest asocjacyjny .
Obliczenia monadyczne
Załóżmy, że chcemy wybrać i pracować z jakąś specjalną kategorią obliczeń, której wynik zawiera coś więcej niż tylko jedną zwracaną wartość. Nie chcemy sprecyzować, co oznacza „coś więcej”, chcemy zachować jak najbardziej ogólną sytuację. Najbardziej ogólnym sposobem przedstawienia „czegoś więcej” jest przedstawienie go jako funkcji typu - rodzaju m
rodzaju * -> *
(tzn. Konwertuje jeden typ na inny). Tak więc dla każdej kategorii obliczeń, z którymi chcemy pracować, będziemy mieli jakąś funkcję typu m :: * -> *
. (W Haskell, m
jest []
, IO
, Maybe
, itd.) I kategoria wola zawiera wszystkie funkcje typów a -> m b
.
Teraz chcielibyśmy pracować z funkcjami w takiej kategorii w taki sam sposób, jak w przypadku podstawowym. Chcemy móc komponować te funkcje, chcemy, aby kompozycja była asocjacyjna i chcemy mieć tożsamość. Potrzebujemy:
- Aby mieć operatora (nazwijmy go
<=<
), który komponuje funkcje f :: a -> m b
i tworzy g :: b -> m c
coś takiego g <=< f :: a -> m c
. I musi być asocjacyjny.
- Aby mieć jakąś funkcję tożsamości dla każdego typu, nazwijmy ją
return
. Chcemy również, aby to f <=< return
było to samo f
i co return <=< f
.
Każdy, m :: * -> *
dla którego mamy takie funkcje return
i <=<
nazywa się monadą . Pozwala nam tworzyć złożone obliczenia z prostszych, tak jak w przypadku podstawowym, ale teraz typy wartości zwracanych są przekształcane przez m
.
(Właściwie nieco nadużyłem tutaj terminu kategoria . W sensie teorii kategorii możemy nazwać naszą konstrukcję kategorią tylko wtedy, gdy wiemy, że przestrzega ona tych praw.)
Monady w Haskell
W Haskell (i innych językach funkcjonalnych) przeważnie pracujemy z wartościami, a nie z funkcjami typów () -> a
. Zamiast definiować <=<
dla każdej monady, definiujemy funkcję (>>=) :: m a -> (a -> m b) -> m b
. Taka alternatywna definicja jest równoważna, możemy wyrazić >>=
za pomocą <=<
i odwrotnie (spróbuj jako ćwiczenie lub zobacz źródła ). Zasada jest teraz mniej oczywista, ale pozostaje ta sama: nasze wyniki są zawsze typami, m a
a my tworzymy funkcje typów a -> m b
.
W przypadku każdej monady, którą tworzymy, nie możemy zapominać o sprawdzeniu tego return
i <=<
posiadaniu wymaganych właściwości: asocjatywności i tożsamości lewej / prawej. Wyrażone za pomocą return
i >>=
nazywane są prawami monady .
Przykład - listy
Jeśli zdecydujemy m
się być []
, otrzymamy kategorię funkcji typów a -> [b]
. Takie funkcje reprezentują obliczenia niedeterministyczne, których wynikiem może być jedna lub więcej wartości, ale także żadnych wartości. Daje to początek tak zwanej monadzie listy . Skład f :: a -> [b]
i g :: b -> [c]
działa w następujący sposób: g <=< f :: a -> [c]
oznacza obliczenie wszystkich możliwych wyników typu [b]
, zastosowanie g
do każdego z nich i zebranie wszystkich wyników na jednej liście. Wyrażony w Haskell
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
lub używając >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Zauważ, że w tym przykładzie typy zwracane były, [a]
więc możliwe, że nie zawierały żadnej wartości typu a
. Rzeczywiście nie ma takiego wymogu dla monady, aby typ zwracany miał takie wartości. Niektóre monady zawsze mają (jak IO
lub State
), ale niektóre nie, jak []
lub Maybe
.
Monada IO
Jak wspomniałem, IO
monada jest nieco wyjątkowa. Wartość typu IO a
oznacza wartość typu a
zbudowaną przez interakcję ze środowiskiem programu. Tak więc (w przeciwieństwie do wszystkich innych monad) nie możemy opisać wartości typu IO a
za pomocą czystej konstrukcji. Oto IO
po prostu tag lub etykieta, która odróżnia obliczenia oddziałujące ze środowiskiem. Jest to (jedyny przypadek), w którym widoki nr 1 i nr 2 są poprawne.
W przypadku IO
monady:
- Skład
f :: a -> IO b
i g :: b -> IO c
środki: Oblicz, f
który wchodzi w interakcje ze środowiskiem, a następnie oblicz, g
który używa wartości i oblicza wynik interakcji z otoczeniem.
return
po prostu dodaje do wartości IO
„tag” (po prostu „obliczamy” wynik, utrzymując nienaruszone środowisko).
- Prawa monady (asocjatywność, tożsamość) są gwarantowane przez kompilator.
Niektóre uwagi:
- Ponieważ obliczenia monadyczne zawsze mają typ wyniku
m a
, nie ma sposobu na „ucieczkę” od IO
monady. Oznacza to, że: gdy obliczenia wchodzą w interakcje ze środowiskiem, nie można zbudować obliczeń, które by nie działały.
- Kiedy programista funkcjonalna nie wie, jak zrobić coś w sposób czysty, (S) może on (jako ostatni ośrodek) Zaprogramuj zadanie jakiejś stanowej obliczeń w
IO
monady. Dlatego IO
często nazywany jest bin sinem programisty .
- Zauważ, że w nieczystym świecie (w sensie programowania funkcjonalnego) odczyt wartości może również zmienić środowisko (np. Zużywać dane użytkownika). Dlatego takie funkcje
getChar
muszą mieć typ wyniku IO something
.