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.
ApplicativeKlasa typ siedzi gdzieś pomiędzy Functori Monadtak można by oczekiwać, że mają podobną podstawę matematycznego. Dokumentacja Control.Applicativemoduł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
. . .
MonadMyś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 Applicativei dlaczego tak to nazywamy?
Co Applicativewynosi 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 fmappozwala nam podnieść się notz pracy Booldo 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 Booldo Maybew 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 Monadinstancji:
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.Monadzapewnia 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
Functorjest 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.Monadmoduł udostępnia również funkcję, która robi to samo dla monad:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Gdzie apjest oczywiście skrótem od „aplikuj”. Ponieważ każdy Monadmoże być Applicativei appotrzebuje 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 Functorpodnoszenia nazywa się, fmapponieważ jest uogólnieniem mapoperacji 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 Functortego miejsca zachowamy ideę struktury niezależnej od jej parametru typu , a od Monoidtego 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” Functori nie możemy wyczarować wartości dowolnego typu, więc naprawimy typ mfEmptyas f ().
Nie chcemy również wymuszać mfAppendkoniecznoś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 mfAppendjest teraz wyraźnie uogólniona wersja ziplisty 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 purejest 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 zipWithlisty, więc można to czytać jako „funktory zip z”, podobnie do czytania fmapjako „mapuj funktor z”.
Pierwsza jest bliższa intencji Applicativeklasy 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 Monaddo istniejącego obliczenia
Wszystkie trzy są w istocie zwykłymi aplikacjami funkcyjnymi, nieco doprawionymi.