Przepraszam, naprawdę nie znam swojej matematyki, więc jestem ciekawy, jak wymawiać funkcje w typeklasie Applicative
Myślę, że znajomość matematyki, czy nie, jest tutaj w dużej mierze nieistotna. Jak zapewne wiesz, Haskell pożycza kilka fragmentów terminologii z różnych dziedzin abstrakcyjnej matematyki, w szczególności z teorii kategorii , skąd mamy funktory i monady. Użycie tych terminów w Haskell różni się nieco od formalnych definicji matematycznych, ale i tak są one na tyle bliskie, że i tak są dobrymi terminami opisowymi.
Applicative
Klasa typ siedzi gdzieś pomiędzy Functor
i Monad
tak można by oczekiwać, że mają podobną podstawę matematycznego. Dokumentacja Control.Applicative
modułu zaczyna się od:
Moduł ten opisuje strukturę pośrednią między funktorem a monadą: dostarcza czystych wyrażeń i sekwencjonowania, ale bez wiązania. (Technicznie, silny, luźny, monoidalny funktor.)
Hmm.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Monad
Myślę, że nie tak chwytliwe, jak .
Wszystko to w zasadzie sprowadza się do tego, Applicative
że nie pasuje do żadnej szczególnie interesującej koncepcji matematycznie, więc nie ma gotowych terminów, które opisują sposób, w jaki jest używany w Haskell. Więc na razie odłóż na bok matematykę.
Jeśli chcemy wiedzieć, jak zadzwonić (<*>)
, może pomóc wiedzieć, co to w zasadzie oznacza.
Więc o co chodzi Applicative
i dlaczego tak to nazywamy?
Co Applicative
wynosi w praktyce jest to sposób, aby podnieść dowolne funkcje w Functor
. Rozważmy kombinację Maybe
(prawdopodobnie najprostszy nietrywialny Functor
) i Bool
(podobnie jak najprostszy nietrywialny typ danych).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
Ta funkcja fmap
pozwala nam podnieść się not
z pracy Bool
do pracy Maybe Bool
. Ale co, jeśli chcemy podnieść (&&)
?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Cóż, to nie jest to, co chcemy w ogóle ! W rzeczywistości jest to prawie bezużyteczne. Możemy starać się być mądry i podkraść innego Bool
do Maybe
w tył ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... ale to nie jest dobre. Po pierwsze, jest źle. Po drugie, jest brzydki . Moglibyśmy próbować dalej, ale okazuje się, że nie ma sposobu, aby podnieść funkcję wielu argumentów do pracy na dowolnejFunctor
. Denerwujący!
Z drugiej strony, możemy to zrobić z łatwością, jeśli użyliśmy Maybe
„s Monad
instancji:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Teraz to dużo kłopotów, żeby przetłumaczyć prostą funkcję - dlatego Control.Monad
zapewnia funkcję, aby to zrobić automatycznie liftM2
. Dwójka w nazwie odnosi się do faktu, że działa na funkcjach o dokładnie dwóch argumentach; podobne funkcje istnieją dla funkcji 3, 4 i 5 argumentowych. Te funkcje są lepsze , ale nie doskonałe, a określanie liczby argumentów jest brzydkie i niezdarne.
To prowadzi nas do artykułu, który wprowadził klasę typu Applicative . W nim autorzy dokonują zasadniczo dwóch obserwacji:
- Przeniesienie funkcji wieloargumentowych do a
Functor
jest rzeczą bardzo naturalną
- Nie wymaga to pełnych możliwości pliku
Monad
Normalna aplikacja funkcji jest napisana przez proste zestawienie terminów, więc aby uczynić "podniesioną aplikację" tak prostą i naturalną, jak to tylko możliwe, w artykule przedstawiono operatory wrostków, które zastępują aplikację, przeniesione doFunctor
i klasy typu, aby zapewnić to, co jest do tego potrzebne .
Wszystko to prowadzi nas do następującego punktu: (<*>)
po prostu reprezentuje aplikację funkcji - dlaczego więc wymawiać ją inaczej niż „operator zestawienia” z białymi znakami?
Ale jeśli to nie jest zbyt satysfakcjonujące, możemy zauważyć, że Control.Monad
moduł udostępnia również funkcję, która robi to samo dla monad:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Gdzie ap
jest oczywiście skrótem od „aplikuj”. Ponieważ każdy Monad
może być Applicative
i ap
potrzebuje tylko podzbioru funkcji obecnych w tym drugim, możemy być może powiedzieć, że gdyby (<*>)
nie był operatorem, należałoby go wywołać ap
.
Możemy też podejść do rzeczy z innej strony. Operacja Functor
podnoszenia nazywa się, fmap
ponieważ jest uogólnieniem map
operacji na listach. Jak wyglądałaby funkcja na listach (<*>)
? Jest coap
działa na listach, ale samo w sobie nie jest to szczególnie przydatne.
W rzeczywistości istnieje być może bardziej naturalna interpretacja list. Co przychodzi na myśl, gdy patrzysz na następujący podpis typu?
listApply :: [a -> b] -> [a] -> [b]
Jest coś tak kuszącego w pomyśle równoległego zestawiania list i przypisywania każdej funkcji z pierwszej do odpowiedniego elementu drugiej. Niestety dla naszego starego przyjaciela Monad
, ta prosta operacja narusza prawa monady, jeśli listy mają różną długość. Ale to dobrze Applicative
, w takim przypadku (<*>)
staje się sposobem na połączenie razem uogólnionej wersji zipWith
, więc może możemy sobie wyobrazić nazywanie tego fzipWith
?
Ten pomysł na spakowanie faktycznie prowadzi nas do pełnego koła. Pamiętasz te matematyczne rzeczy wcześniej, o monoidalnych funktorach? Jak sama nazwa wskazuje, są to sposoby łączenia budowy monoidów i funktorów, z których oba są znanymi klasami typu Haskell:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Jak by wyglądały, gdybyś umieścił je razem w pudełku i trochę nim potrząsnął? Z Functor
tego miejsca zachowamy ideę struktury niezależnej od jej parametru typu , a od Monoid
tego zachowamy ogólną postać funkcji:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Nie chcemy zakładać, że istnieje sposób na stworzenie prawdziwie „pustego” Functor
i nie możemy wyczarować wartości dowolnego typu, więc naprawimy typ mfEmpty
as f ()
.
Nie chcemy również wymuszać mfAppend
konieczności posiadania spójnego parametru typu, więc teraz mamy to:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
Jaki jest typ wyniku mfAppend
? Mamy dwa dowolne typy, o których nic nie wiemy, więc nie mamy wielu opcji. Najrozsądniej jest zachować jedno i drugie:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
W którym momencie mfAppend
jest teraz wyraźnie uogólniona wersja zip
listy i możemy Applicative
łatwo zrekonstruować :
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
To również pokazuje nam, że pure
jest powiązany z elementem tożsamości a Monoid
, więc innymi dobrymi nazwami może być wszystko, co sugeruje wartość jednostkową, operację zerową lub tym podobne.
To było długie, więc podsumowując:
(<*>)
jest po prostu zmodyfikowaną aplikacją funkcyjną, więc możesz ją odczytać jako „ap” lub „zastosować”, lub całkowicie usunąć ją w taki sam sposób, jak zwykłą aplikację funkcyjną.
(<*>)
również z grubsza uogólnia zipWith
listy, więc można to czytać jako „funktory zip z”, podobnie do czytania fmap
jako „mapuj funktor z”.
Pierwsza jest bliższa intencji Applicative
klasy typu - jak sama nazwa wskazuje - więc to właśnie polecam.
W rzeczywistości zachęcam do liberalnego używania i braku wymowy wszystkich operatorów podniesionych aplikacji :
(<$>)
, która przenosi funkcję jednoargumentową do pliku Functor
(<*>)
, który łączy funkcję wieloparametrową za pomocą pliku Applicative
(=<<)
, który wiąże funkcję, która wprowadza a Monad
do istniejącego obliczenia
Wszystkie trzy są w istocie zwykłymi aplikacjami funkcyjnymi, nieco doprawionymi.