Haskell Language
Monady
Szukaj…
Wprowadzenie
Monada to typ danych możliwych do skomponowania działań. Monad
to klasa konstruktorów typów, których wartości reprezentują takie akcje. Może IO
jest najbardziej rozpoznawalny jeden: wartość IO a
to „recepta na zdobycie się a
wartość od realnego świata”.
Mówimy, że konstruktor typów m
(taki jak []
lub Maybe
) tworzy monadę, jeśli istnieje instance Monad m
spełniająca określone prawa dotyczące kompozycji działań. Możemy wtedy rozumować ma
jako „akcję, której wynik ma typ a
”.
Może monada
Maybe
być używany do reprezentowania możliwie pustych wartości - podobnych do null
w innych językach. Zwykle jest używany jako typ wyjściowy funkcji, które mogą w jakiś sposób zawieść.
Rozważ następującą funkcję:
halve :: Int -> Maybe Int
halve x
| even x = Just (x `div` 2)
| odd x = Nothing
Pomyśl o halve
jako akcji, w zależności od Int
, która próbuje zmniejszyć liczbę całkowitą o połowę, ale jeśli jest nieparzysta, kończy się niepowodzeniem.
Jak trzy razy halve
liczbę całkowitą o halve
?
takeOneEighth :: Int -> Maybe Int -- (after you read the 'do' sub-section:)
takeOneEighth x =
case halve x of -- do {
Nothing -> Nothing
Just oneHalf -> -- oneHalf <- halve x
case halve oneHalf of
Nothing -> Nothing
Just oneQuarter -> -- oneQuarter <- halve oneHalf
case halve oneQuarter of
Nothing -> Nothing -- oneEighth <- halve oneQuarter
Just oneEighth ->
Just oneEighth -- return oneEighth }
-
takeOneEighth
to sekwencja trzech kroków ohalve
połączonych ze sobą. - Jeśli krok o
halve
nie powiedzie, chcemy, aby cała kompozycjatakeOneEighth
. - Jeśli krok o
halve
powiedzie, chcemy przesłać jego wynik do przodu.
instance Monad Maybe where
-- (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>= f = Nothing -- infixl 1 >>=
Just x >>= f = Just (f x) -- also, f =<< m = m >>= f
-- return :: a -> Maybe a
return x = Just x
a teraz możemy napisać:
takeOneEighth :: Int -> Maybe Int
takeOneEighth x = halve x >>= halve >>= halve -- or,
-- return x >>= halve >>= halve >>= halve -- which is parsed as
-- (((return x) >>= halve) >>= halve) >>= halve -- which can also be written as
-- (halve =<<) . (halve =<<) . (halve =<<) $ return x -- or, equivalently, as
-- halve <=< halve <=< halve $ x
Skład Kleisli <=<
jest zdefiniowany jako (g <=< f) x = g =<< fx
lub równoważnie jako (f >=> g) x = fx >>= g
. Dzięki niemu powyższa definicja staje się sprawiedliwa
takeOneEighth :: Int -> Maybe Int
takeOneEighth = halve <=< halve <=< halve -- infixr 1 <=<
-- or, equivalently,
-- halve >=> halve >=> halve -- infixr 1 >=>
Istnieją trzy prawa monad, których powinna przestrzegać każda monada, to znaczy każdy typ, który jest instancją klasy Monad
:
1. return x >>= f = f x
2. m >>= return = m
3. (m >>= g) >>= h = m >>= (\y -> g y >>= h)
gdzie m
jest monadą, f
ma typ a -> mb
a g
ma typ b -> mc
.
Lub równoważnie, stosując >=>
Kleisli operatora kompozycję określoną powyżej:
1. return >=> g = g -- do { y <- return x ; g y } == g x
2. f >=> return = f -- do { y <- f x ; return y } == f x
3. (f >=> g) >=> h = f >=> (g >=> h) -- do { z <- do { y <- f x; g y } ; h z }
-- == do { y <- f x ; do { z <- g y; h z } }
Przestrzeganie tych praw znacznie ułatwia rozumowanie monady, ponieważ gwarantuje, że używanie funkcji monadycznych i ich komponowanie zachowuje się w rozsądny sposób, podobnie jak w przypadku innych monad.
Sprawdźmy, czy monada Maybe
przestrzega trzech praw monady.
- Lewe prawo tożsamości -
return x >>= f = fx
return z >>= f
= (Just z) >>= f
= f z
- Właściwe prawo tożsamości -
m >>= return = m
-
Just
konstruktor danych
Just z >>= return
= return z
= Just z
-
Nothing
konstruktora danych
Nothing >>= return
= Nothing
- Prawo asocjacyjne -
(m >>= f) >>= g = m >>= (\x -> fx >>= g)
-
Just
konstruktor danych
-- Left-hand side
((Just z) >>= f) >>= g
= f z >>= g
-- Right-hand side
(Just z) >>= (\x -> f x >>= g)
(\x -> f x >>= g) z
= f z >>= g
-
Nothing
konstruktora danych
-- Left-hand side
(Nothing >>= f) >>= g
= Nothing >>= g
= Nothing
-- Right-hand side
Nothing >>= (\x -> f x >>= g)
= Nothing
Monada IO
Nie ma sposobu na uzyskanie wartości typu a
z wyrażenia typu IO a
i nie powinno być. Jest to tak naprawdę duża część tego, dlaczego monady są używane do modelowania IO
.
Wyrażenie typu IO a
może być traktowane jako reprezentujące działanie, które może wchodzić w interakcje ze światem rzeczywistym, a jeśli zostanie wykonane, spowoduje coś typu a
. Na przykład funkcja getLine :: IO String
z preludium nie oznacza, że pod getLine
jest jakiś określony ciąg, który mogę wyodrębnić - oznacza to, że getLine
reprezentuje działanie polegające na uzyskaniu linii ze standardowego wejścia.
Nic dziwnego, że main :: IO ()
ponieważ program Haskell reprezentuje obliczenia / działania, które współdziałają ze światem rzeczywistym.
Rzeczy, które możesz zrobić z wyrażeniami typu IO a
ponieważ IO
to monada:
Wykonaj dwie akcje za pomocą
(>>)
aby utworzyć nową akcję, która wykonuje pierwszą akcję, odrzuca wszelką wytworzoną wartość, a następnie wykonuje drugą akcję.-- print the lines "Hello" then "World" to stdout putStrLn "Hello" >> putStrLn "World"
Czasami nie chcesz odrzucić wartości, która została wygenerowana w pierwszej akcji - tak naprawdę chciałbyś, aby została wykorzystana w drugiej akcji. Do tego mamy
>>=
. DlaIO
ma typ(>>=) :: IO a -> (a -> IO b) -> IO b
.-- get a line from stdin and print it back out getLine >>= putStrLn
Weź normalną wartość i przekonwertuj ją na działanie, które natychmiast zwraca wartość, którą jej podałeś. Funkcja ta jest mniej przydatna oczywiście aż zaczniesz używać
do
notacji.-- make an action that just returns 5 return 5
Więcej z Wiki Haskell na monadzie IO tutaj .
Lista Monada
Listy tworzą monadę. Mają instancję monady odpowiadającą tej:
instance Monad [] where return x = [x] xs >>= f = concat (map f xs)
Możemy ich użyć do naśladowania niedeterminizmu w naszych obliczeniach. Kiedy używamy xs >>= f
, funkcja f :: a -> [b]
jest mapowana na listę xs
, uzyskując listę list wyników każdej aplikacji f
na każdym elemencie xs
i wszystkich listach wyniki są następnie łączone w jedną listę wszystkich wyników. Jako przykład obliczamy sumę dwóch liczb niedeterministycznych przy użyciu notacji , przy czym suma jest reprezentowana przez listę sum wszystkich par liczb całkowitych z dwóch list, przy czym każda lista reprezentuje wszystkie możliwe wartości liczby niedeterministycznej:
sumnd xs ys = do
x <- xs
y <- ys
return (x + y)
Lub równoważnie, używając liftM2
w Control.Monad
:
sumnd = liftM2 (+)
otrzymujemy:
> sumnd [1,2,3] [0,10]
[1,11,2,12,3,13]
Monada jako podklasa aplikacyjna
Począwszy od GHC 7.10, Applicative
jest nadklasą Monad
(tzn. Każdy typ, który jest Monad
musi być także Applicative
). Wszystkie metody Applicative
( pure
, <*>
) można zaimplementować w kategoriach metod Monad
( return
, >>=
).
Oczywiste jest, że pure
i return
służą równoważnym celom, więc pure = return
. Definicja <*>
jest zbyt jasna:
mf <*> mx = do { f <- mf; x <- mx; return (f x) }
-- = mf >>= (\f -> mx >>= (\x -> return (f x)))
-- = [r | f <- mf, x <- mx, r <- return (f x)] -- with MonadComprehensions
-- = [f x | f <- mf, x <- mx]
Ta funkcja jest zdefiniowana jako ap
w standardowych bibliotekach.
Dlatego jeśli już zdefiniowałeś wystąpienie Monad
dla typu, możesz skutecznie uzyskać wystąpienie Applicative
dla niego „za darmo” poprzez zdefiniowanie
instance Applicative < type > where
pure = return
(<*>) = ap
Podobnie jak w przypadku praw monady, te równoważności nie są egzekwowane, ale programiści powinni upewnić się, że są zawsze przestrzegani.
Nie ma ogólnego sposobu na wyodrębnienie wartości z obliczeń monadycznych
Możesz zawijać wartości w akcje i przesyłać wyniki jednego obliczenia do drugiego:
return :: Monad m => a -> m a
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Jednak definicja Monady nie gwarantuje istnienia funkcji typu Monad m => ma -> a
.
Oznacza to, że generalnie nie ma możliwości wyodrębnienia wartości z obliczeń (tj. „Rozpakowanie”). Dzieje się tak w wielu przypadkach:
extract :: Maybe a -> a
extract (Just x) = x -- Sure, this works, but...
extract Nothing = undefined -- We can’t extract a value from failure.
W szczególności nie ma funkcji IO a -> a
, która często myli początkujących; zobacz ten przykład .
notacja
do
-notation to cukier składniowy dla monad. Oto zasady:
do x <- mx do x <- mx y <- my is equivalent to do y <- my ... ...
do let a = b let a = b in ... is equivalent to do ...
do m m >> ( e is equivalent to e)
do x <- m m >>= (\x -> e is equivalent to e)
do m is equivalent to m
Na przykład te definicje są równoważne:
example :: IO Integer
example =
putStrLn "What's your name?" >> (
getLine >>= (\name ->
putStrLn ("Hello, " ++ name ++ ".") >> (
putStrLn "What should we return?" >> (
getLine >>= (\line ->
let n = (read line :: Integer) in
return (n + n))))))
example :: IO Integer
example = do
putStrLn "What's your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ ".")
putStrLn "What should we return?"
line <- getLine
let n = (read line :: Integer)
return (n + n)
Definicja Monady
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
Najważniejszą funkcją do obsługi monad jest operator bind >>=
:
(>>=) :: m a -> (a -> m b) -> m b
- Pomyśl
ma
jako „działania zea
wyniku”. - Pomyśl
a -> mb
„działanie (w zależności na jaka
parametrze) zb
. Wynik” .
>>=
sekwencjonuje dwie akcje razem, przesyłając wyniki z pierwszej akcji do drugiej.
Inną funkcją zdefiniowaną przez Monad
jest:
return :: a -> m a
Jego nazwa jest niefortunna: ten return
nie ma nic wspólnego ze słowem kluczowym return
znalezionym w imperatywnych językach programowania.
return x
jest trywialną akcją dającą x
jako wynik. (Jest to trywialne w następującym znaczeniu :)
return x >>= f ≡ f x -- “left identity” monad law
x >>= return ≡ x -- “right identity” monad law