Autor: Aditya Bhargava
Oryginalny post: Functors, Applicatives, And Monads In Pictures
Mamy prostą wartość (ang. value):
I wiemy, jak zastosować do tej wartości funkcję:
Dość proste. Powiedzmy więc, że wartość może być umieszczona w kontekście. Za kontekst przyjmijmy pudełko, do którego została zapakowana:
Teraz wywołując funkcję otrzymamy różne wyniki w zależności od kontekstu. To na tej idei oparte są funktory (ang. functor, w Haskellu Functor
), funktory aplikatywne (ang. applicative functor, Applicative
), monady (ang. monad, Monad
) itp. Typ danych Maybe
(pol. [być] może) definiuje dwa powiązane konteksty:
Za chwilę zorientujemy się jak różny jest wynik wywołania w zależności od tego, czy argumentem jest Just a
czy Nothing
. Jednak najpierw wspomnijmy o funktorach.
Gdy wartość jest zapakowana w kontekst, nie można na niej wykonać normalnej funkcji:
W takim przypadku przydaje się fmap
, bo jest na bieżąco z kontekstami. fmap
wie, jak wywoływać funkcje na argumentach opakowanych w kontekst. Dla przykładu żałóżmy, że chcemy wywołać (+3)
na Just 2
. Korzystając z fmap
:
> fmap (+3) (Just 2)
Just 5
Bum! fmap
pokazał nam jak to się robi! Ale skąd wie, jak wywołać funkcję?
Functor
?Functor
jest typeklasą. Oto jego definicja:
fmap
będzie na nim operował.Functor
em jest każdy typ danych, który definiuje, w jaki sposób operuje na nim fmap
. Oto jak działa fmap
:
(+3)
)Just 2
),Just 5
).Możemy więc zrobić tak:
> fmap (+3) (Just 2)
Just 5
I fmap
magicznie wykonuje funkcję, bo Maybe
jest Functor
em. Specyfikuje, w jaki sposób fmap
stostuje funkcję wobec Just
–ów i Nothing
–ów:
instance Functor Maybe where
fmap func (Just val) = Just (func val)
fmap func Nothing = Nothing
Oto co dzieje się za kulisami, gdy piszemy fmap (+3) (Just 2)
:
A co, gdybyśmy kazali fmap
owi zastosować (+3)
na Nothing
?
> fmap (+3) Nothing
Nothing
Nic (nie) wchodzi, nic (nie) wychodzi
Tego nie ogarniesz!
Jak Morfeusz w Matrixie, fmap
po prostu wie, co trzeba robić. Zaczynasz z Nothing
i kończysz z Nothing
! Staje się jasne, dlaczego powstał typ Maybe
. Dla przykładu, w języku bez Maybe
można by pracować z rekordem bazy danych w poniższy sposób:
post = Post.find_by_id(1)
if post
return post.title
else
return nil
end
Jednak w Haskellu można tak:
fmap (getPostTitle) (findPost 1)
Jeśli findPost
znajdzie posta, dostaniemy jego tytuł z getPostTitle
. Natomiast jeśli zwróci Nothing
, dostaniemy Nothing
! Nieźle, co? <$>
jest infiksową wersją fmap
, więc częściej możesz spotkać taką konstrukcję:
getPostTitle <$> (findPost 1)
Weźmy inny przykład. Co stanie się, gdy wywołamy funkcję na liście?
Listy też są funktorami. Oto definicja:
instance Functor [] where
fmap = map
No, to jeszcze ostatni przykład. Co stanie się, gdy do funkcji przekażemy inną funkcję?
fmap (+3) (+1)
Tu funkcja:
A tu funkcja wywołana z funkcją jako argumentem:
Wynikiem jest po prostu inna funkcja!
> import Control.Applicative
> let foo = fmap (+3) (+2)
> foo 10
15
A więc funkcje też są Functor
ami!
instance Functor ((->) r) where
fmap f g = f . g
Używając fmap
na funkcji, tak naprawdę po prostu komponujesz funkcje!
Funktory aplikatywne (Applicative
) wchodzą na nieco wyższy poziom. Podobnie jak ze zwyczajnymi funktorami (Functor
), nasze wartości są opakowane w kontekst:
Jednak także nasze funkcje także są opakowane w kontekst!
Tak, niech to zapadnie w pamięć. Funktory aplikatywne się nie patyczkują. Control.Applicative
definiuje <*>
, który wie, jak zastosować opakowaną w kontekst funkcję do zapakowanej wartości:
To jest:
Just (+3) <*> Just 2 == Just 5
Użycie <*>
może doprowadzić do interesujących sytuacji, np.:
> [(*2), (+3)] <*> [1, 2, 3]
[2, 4, 6, 4, 5, 6]
A teraz coś, czego nie można zrobić zwykłym funktorem, ale można funktorem aplikatywnym. Jak wykonać funkcję dwóch zmiennych na dwóch zapakowanych wartościach?
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <$> (Just 4)
ERROR ??? CO TO W OGÓLE MA ZNACZYĆ CZEMU FUNKCJA JEST ZAPAKOWANA W JUST
Funktor aplikatywny:
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <*> (Just 3)
Just 8
Applicative
odsuwa Functor
na bok. “Duzi chłopcy mogą używać funkcji z dowolną liczbą argumentów” – mówi. “Uzbrojony w <$>
i <*>
mogę wziąć dowolną funkcję oczekującą dowolną liczbę nieopakowanych wartości. Przekazuję jej wszystkie wartości zapakowane i otrzymuję zapakowany wynik! AHAHAHAHAH!”
> (*) <$> Just 5 <*> Just 3
Just 15
Jest też funkcja nazwana liftA2
, która robi dokładnie to samo:
> liftA2 (*) (Just 5) (Just 3)
Just 15
Jak nauczyć się o monadach:
Monady dodają coś jeszcze.
Funktory wykonują funkcję na opakowanej wartości:
Funktory aplikatywne wykonują opakowaną funkcję na opakowanej wartości:
Monady wykonują funkcję, która zwraca opakowaną wartość na opakowanej wartości. Monady mają w tym celu funkcję >>=
(czyt. “bind”).
Zobaczmy przykład. Znane nam Maybe
jest monadą:
Przyjmijmy, że half
jest funkcją działającą tylko na liczbach parzystych:
half x = if even x
then Just (x `div` 2)
else Nothing
Co gdybyśmy podali jej zapakowaną wartość?
Musimy użyć >>=
, żeby wepchnąć naszą zapakowaną wartość do tej funkcji. Oto zdjęcie >>=
:
A oto jak działa:
> Just 3 >>= half
Nothing
> Just 4 >>= half
Just 2
> Nothing >>= half
Nothing
Co dzieje się w środku? Monad
(monada) jest kolejną typeklasą. Oto część jej definicji:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
Gdzie >>=
to:
>>=
bierze monadę (jak Just 3
)half
)Więc Maybe
jest monadą:
instance Monad Maybe where
Nothing >>= func = Nothing
Just val >>= func = func val
A tutaj w akcji z Just 3
!
>>=
(czyt. “bind”) odpakowuje wartość,A gdy przkażesz na wejściu Nothing
, jest jeszcze prościej:
Wywołania można też połączyć w łańcuch:
> Just 20 >>= half >>= half >>= half
Nothing
Nieźle! Już wiemy, że Maybe
jest Functor
em, Applicative
m i Monad
ą. Teraz przejdźmy do innego przykładu: monady IO
:
Skupimy się na trzech funkcjach. getLine
nie bierze żadnych argumentów i pobiera wejście od użytkownika:
getLine :: IO String
readFile
bierze ciąg znaków (nazwę pliku) i zwraca zawartość pliku:
readFile :: FilePath -> IO String
putStrLn
bierze ciąg znaków i wypisuje go:
putStrLn :: String -> IO ()
Wszystkie te trzy funkcje biorą zwyczajną wartość (bądź żadną) i zwracają opakowaną wartość. Możemy wywołać je wszystkie w łańcuchu korzystając z >>=
!
getLine >>= readFile >>= putStrLn
O, tak! Pierwszy rząd na występach monad!
Haskell dostarcza także cukier składniowy do operacji na monadach, zwany do
notation, notacją do
:
foo = do
filename <- getLine
contents <- readFile filename
putStrLn contents
Functor
.Applicative
.Monad
.Maybe
implementuje wszystkie te trzy, jest więc funktorem, funktorem aplikatywnym i monadą.Jakie są między nimi różnice?
fmap
bądź <$>
.<*>
bądź liftA
.>>=
bądź liftM
.Więc, drogi kumplu (myślę, że na tym etapie jesteśmy już kumplami), myślę, że zgodzimy się oboje, że monady są łatwe i że są DOBRYM POMYSŁEM™. Skoro już napociłeś się czytając ten przewodnik, czemu by nie pójść na całość? Przejrzyj rozdział o monadach w Learn You A Haskell (ang). Jest wiele rzeczy, które pominąłem, bo Miran świetnie wgłębia się w ten temat.
Od tłumacza: a jeśli masz jeszcze chwilkę, może zainteresują Cię moje projekty. ☺