Podczas nauki języka Haskell spotkałem się z wieloma samouczkami próbującymi wyjaśnić, czym są monady i dlaczego monady są ważne w Haskell. Każda z nich używała analogii, więc łatwiej byłoby uchwycić sens. Ostatecznie mam 3 różne poglądy na to, czym jest monada:
Widok 1: Monada jako etykieta
Czasami myślę, że monada jako etykieta określonego typu. Na przykład funkcja typu:
myfunction :: IO Int
moja funkcja jest funkcją, która przy każdym wykonaniu daje wartość Int. Typ wyniku nie jest Int, ale IO Int. Tak więc IO jest etykietą wartości Int ostrzegającą użytkownika, aby wiedział, że wartość Int jest wynikiem procesu, w którym wykonano akcję IO.
W konsekwencji ta wartość Int została oznaczona jako wartość pochodząca z procesu z IO, dlatego ta wartość jest „brudna”. Twój proces nie jest już czysty.
Widok 2: Monada jako prywatna przestrzeń, w której mogą się zdarzyć nieprzyjemne rzeczy.
W systemie, w którym cały proces jest czysty i surowy, czasami trzeba mieć skutki uboczne. Tak więc monada to tylko niewielka przestrzeń, która pozwala ci na robienie nieprzyjemnych efektów ubocznych. W tej przestrzeni możesz uciec z czystego świata, przejść nieczyste, dokonać procesu, a następnie powrócić z wartością.
Widok 3: Monada jak w teorii kategorii
Tego poglądu nie do końca rozumiem. Monada jest po prostu funktorem tej samej kategorii lub podkategorii. Na przykład masz wartości Int i jako podkategoria IO Int, które są wartościami Int wygenerowanymi po procesie IO.
Czy te widoki są prawidłowe? Który jest bardziej dokładny?
Odpowiedzi:
Widoki 1 i 2 są ogólnie niepoprawne.
* -> *
może działać jako etykieta, monady to znacznie więcej.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ścix :: 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 mamyf :: a -> b
ig :: b -> c
dostaniemyg . f :: a -> c
. Zauważ, że działa to również w przypadku naszych przeliczonych wartości: jeśli mamyx :: a
i przekonwertujemy to na naszą reprezentację, otrzymamyf . ((\_ -> x) :: () -> a) :: () -> b
.Ta reprezentacja ma kilka bardzo ważnych właściwości, a mianowicie:
id :: a -> a
dla każdego typua
. Jest to element tożsamości w odniesieniu do.
:f
jest równyf . id
i doid . f
..
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ę typum :: * -> *
. (W Haskell,m
jest[]
,IO
,Maybe
, itd.) I kategoria wola zawiera wszystkie funkcje typówa -> 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:
<=<
), który komponuje funkcjef :: a -> m b
i tworzyg :: b -> m c
coś takiegog <=< f :: a -> m c
. I musi być asocjacyjny.return
. Chcemy również, aby tof <=< return
było to samof
i coreturn <=< f
.Każdy,
m :: * -> *
dla którego mamy takie funkcjereturn
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 przezm
.(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ówa -> 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ówa -> [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ładf :: a -> [b]
ig :: b -> [c]
działa w następujący sposób:g <=< f :: a -> [c]
oznacza obliczenie wszystkich możliwych wyników typu[b]
, zastosowanieg
do każdego z nich i zebranie wszystkich wyników na jednej liście. Wyrażony w Haskelllub używając
>>=
Zauważ, że w tym przykładzie typy zwracane były,
[a]
więc możliwe, że nie zawierały żadnej wartości typua
. Rzeczywiście nie ma takiego wymogu dla monady, aby typ zwracany miał takie wartości. Niektóre monady zawsze mają (jakIO
lubState
), ale niektóre nie, jak[]
lubMaybe
.Monada IO
Jak wspomniałem,
IO
monada jest nieco wyjątkowa. Wartość typuIO a
oznacza wartość typua
zbudowaną przez interakcję ze środowiskiem programu. Tak więc (w przeciwieństwie do wszystkich innych monad) nie możemy opisać wartości typuIO a
za pomocą czystej konstrukcji. OtoIO
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:f :: a -> IO b
ig :: 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ściIO
„tag” (po prostu „obliczamy” wynik, utrzymując nienaruszone środowisko).Niektóre uwagi:
m a
, nie ma sposobu na „ucieczkę” odIO
monady. Oznacza to, że: gdy obliczenia wchodzą w interakcje ze środowiskiem, nie można zbudować obliczeń, które by nie działały.IO
monady. DlategoIO
często nazywany jest bin sinem programisty .getChar
muszą mieć typ wynikuIO something
.źródło
IO
nie ma specjalnej semantyki z punktu widzenia języka. To nie specjalnego, to zachowuje się jak każdy inny kod. Tylko implementacja biblioteki wykonawczej jest wyjątkowa. Istnieje również specjalny sposób na ucieczkę (unsafePerformIO
). Myślę, że jest to ważne, ponieważ ludzie często myślą oIO
specjalnym elemencie językowym lub deklaratywnym znaczniku. Nie jest.coerce :: a -> b
która konwertuje dowolne dwa typy (aw większości przypadków powoduje awarię programu). Zobacz ten przykład - możesz przekonwertować nawet funkcję naInt
itp.runST :: (forall s. GHC.ST.ST s a) -> a
Widok 1: Monada jako etykieta
„W konsekwencji ta wartość Int została oznaczona jako wartość pochodząca z procesu z IO, dlatego ta wartość jest„ brudna ”.”
„IO Int” zasadniczo nie jest wartością Int (chociaż może być w niektórych przypadkach, np. „Return 3”). Jest to procedura, która generuje pewną wartość Int. Różne wykonania tej „procedury” mogą dawać różne wartości Int.
Monada m jest osadzonym (imperatywnym) „językiem programowania”: w tym języku można zdefiniować niektóre „procedury”. Wartość monadyczna (typu ma) jest procedurą w tym „języku programowania”, która generuje wartość typu a.
Na przykład:
to jakaś procedura, która generuje wartość typu Int.
Następnie:
jest jakaś procedura, która daje dwa (być może różne) Ints.
Każdy taki „język” obsługuje niektóre operacje:
dwie procedury (ma i mb) mogą być „połączone”: możesz stworzyć większą procedurę (ma >> mb) wykonaną z pierwszej, a następnie drugiej;
co więcej, wynik (a) pierwszego może wpływać na drugi (ma >> = \ a -> ...);
procedura (return x) może dać pewną stałą wartość (x).
Różne wbudowane języki programowania różnią się od obsługiwanych przez nich rzeczy, takich jak:
źródło
Nie myl typu monadycznego z klasą monad.
Typ monadyczny (tj. Typ będący instancją klasy monad) rozwiązałby konkretny problem (w zasadzie każdy typ monadyczny rozwiązuje inny): State, Random, Może, IO. Wszystkie są typami z kontekstem (to, co nazywacie „etykietą”, ale nie to czyni ich monadą).
Dla wszystkich potrzebna jest „operacja łączenia z wyborem” (jedna operacja zależy od wyniku poprzedniej). W grę wchodzi klasa monad: niech twój typ (rozwiązanie danego problemu) będzie instancją klasy monad, a problem łańcuchowy zostanie rozwiązany.
Zobacz, co rozwiązuje klasa monad?
źródło