Wnioskodawcy tworzą, monady nie

110

Wnioskodawcy tworzą, monady nie.

Co oznacza powyższe stwierdzenie? A kiedy jedno jest lepsze od drugiego?

missingfaktor
źródło
5
Skąd masz to oświadczenie? Warto zobaczyć jakiś kontekst.
fuz
@FUZxxl: Słyszałem to wielokrotnie od wielu różnych osób, ostatnio od debasishg na Twitterze.
missingfaktor
3
@stephen Tetley: Należy pamiętać, że wiele takich Applicatives są w rzeczywistości cała rodzina od Monads, a mianowicie po jednym dla każdego „kształt” struktury możliwe. ZipListnie jest a Monad, ale ZipLists o stałej długości są. Readerjest wygodnym przypadkiem specjalnym (czy raczej ogólnym?), w którym rozmiar „struktury” jest ustalony jako liczność typu środowiska.
CA McCann
3
@CAMcCann Wszystkie te zwinne aplikacje (obcinane lub wypełniające) ograniczają się do monad, jeśli naprawisz kształt w sposób, który Readersprowadza się do monady aż do izomorfizmu. Po ustaleniu kształtu kontenera skutecznie koduje on funkcję z pozycji, takich jak notatka. Peter Hancock nazywa takie funktory „naperianem”, ponieważ podlegają one prawom logarytmów.
pigworker
4
@stephen tetley: Inne przykłady obejmują zastosowanie stałej monoidy (która jest kompozycją monad, ale nie monady) i zastosowanie jednostkowego opóźnienia (które lepiej nie dopuszczać łączenia).
pigworker

Odpowiedzi:

115

Jeśli porównamy typy

(<*>) :: Applicative a => a (s -> t) -> a s -> a t
(>>=) :: Monad m =>       m s -> (s -> m t) -> m t

otrzymujemy wskazówkę, co oddziela te dwa pojęcia. To (s -> m t)w typie (>>=)pokazuje, że wartość w smoże określać zachowanie obliczenia w m t. Monady pozwalają na interferencję między warstwami wartości i obliczeń. (<*>)Operator nie umożliwia takich zakłóceń: Działanie i argumentów obliczenia nie zależy od wartości. To naprawdę gryzie. Porównać

miffy :: Monad m => m Bool -> m x -> m x -> m x
miffy mb mt mf = do
  b <- mb
  if b then mt else mf

który wykorzystuje wynik jakiegoś efektu do wyboru między dwoma obliczeniami (np. wystrzelenie rakiet i podpisanie rozejmu), podczas gdy

iffy :: Applicative a => a Bool -> a x -> a x -> a x
iffy ab at af = pure cond <*> ab <*> at <*> af where
  cond b t f = if b then t else f

która używa wartości abdo wyboru między wartościami dwóch obliczeń ati af, po przeprowadzeniu obu, być może z tragicznym skutkiem.

Wersja monadyczna polega zasadniczo na dodatkowej mocy (>>=)wyboru obliczeń z wartości, a to może być ważne. Jednak wspieranie tej mocy utrudnia komponowanie monad. Jeśli spróbujemy zbudować „podwójne wiązanie”

(>>>>==) :: (Monad m, Monad n) => m (n s) -> (s -> m (n t)) -> m (n t)
mns >>>>== f = mns >>-{-m-} \ ns -> let nmnt = ns >>= (return . f) in ???

dotarliśmy tak daleko, ale teraz wszystkie nasze warstwy są pomieszane. Mamy n (m (n t)), więc musimy pozbyć się zewnętrznego n. Jak mówi Alexandre C, możemy to zrobić, jeśli mamy odpowiedni

swap :: n (m t) -> m (n t)

permutować do nwewnątrz i joindo drugiego n.

Słabsze „podwójne zastosowanie” jest znacznie łatwiejsze do zdefiniowania

(<<**>>) :: (Applicative a, Applicative b) => a (b (s -> t)) -> a (b s) -> a (b t)
abf <<**>> abs = pure (<*>) <*> abf <*> abs

ponieważ nie ma interferencji między warstwami.

W związku z tym dobrze jest rozpoznać, kiedy naprawdę potrzebujesz dodatkowej mocy Monads i kiedy możesz uciec ze sztywną strukturą obliczeniową, która Applicativeobsługuje.

Zwróć uwagę, że chociaż komponowanie monad jest trudne, może to być więcej niż potrzebujesz. Typ m (n v)wskazuje na obliczanie z m-efektami, a następnie obliczanie z n-efekty do -wartości v, gdzie m-efekty kończą się przed nrozpoczęciem -efekty (stąd potrzeba swap). Jeśli chcesz po prostu przeplatać m-efekty z n-efektami, to o skład może zbyt wiele prosić!

świniarz
źródło
3
Dla niejasnego przykładu stwierdzasz, że „używa wartości ab do wyboru między wartościami dwóch obliczeń at i af, po przeprowadzeniu obu, być może ze skutkiem tragicznym”. Czy leniwa natura Haskella nie chroni cię przed tym? Jeśli mam list = (\ btf -> if b then t else f): [], a następnie wykonuję instrukcję: list <*> pure True <*> pure "hello" <*> pure (błąd "zły"). ... Otrzymuję „cześć” i błąd nigdy się nie pojawia. Ten kod nie jest tak bezpieczny ani kontrolowany jak monada, ale post wydaje się sugerować, że aplikacje powodują ścisłą ocenę. Ogólnie świetny post! Dzięki!
shj
7
Nadal masz efekty obu, ale czysty (błąd „zły”) nie ma żadnego. Z drugiej strony, jeśli spróbujesz iffy (czysta prawda) (czyste „cześć”) (błąd „zły”), pojawi się błąd, którego miffy unika. Co więcej, jeśli spróbujesz czegoś takiego jak niepewna (czysta prawda) (czysta 0) [1,2], otrzymasz [0,0] zamiast [0]. Wnioskodawcy mają pewien rodzaj ścisłości, ponieważ budują ustalone sekwencje obliczeń, ale wartości wynikające z tych obliczeń są nadal leniwie łączone, jak widać.
pigworker
Czy to prawda, że za wszelkie monad mi nzawsze można napisać transformator monada mt, a działają w n (m t)użyciu mt n t? Czyli zawsze możesz skomponować monady, to jest po prostu bardziej skomplikowane, używając transformatorów?
ron
4
Takie transformatory często istnieją, ale o ile wiem, nie ma kanonicznego sposobu ich generowania. Często istnieje prawdziwy wybór, jak rozwiązać przeplatane efekty z różnych monad, a klasycznym przykładem są wyjątki i stan. Czy stan wycofania wyjątku powinien się zmienić, czy nie? Obie opcje mają swoje miejsce. Powiedziawszy to, istnieje „wolna monada”, która wyraża „arbitralne przeplatanie”. data Free f x = Ret x | Do (f (Free f x)), więc data (:+:) f g x = Inl (f x) | Tnr (g x)i zastanów się Free (m :+: n). To opóźnia wybór sposobu uruchamiania przeplotów.
pigworker
@pigworker Odnośnie leniwej / surowej debaty. Myślę, że w przypadku aplikacji nie można kontrolować efektu z poziomu obliczeń, ale warstwa efektów może bardzo dobrze zdecydować, aby nie oceniać późniejszych wartości. W przypadku parserów (aplikacyjnych) oznacza to, że jeśli parser zawodzi wcześnie, kolejne parsery nie są oceniane / stosowane na wejściu. Na Maybeten oznacza, że wczesna Nothingbędzie tłumić oceny apóźniejszego / kolejnego Just a. Czy to jest poprawne?
ziggystar
75

Wnioskodawcy tworzą, monady nie.

Monady zrobić komponować, ale wynik może nie być monada. W przeciwieństwie do tego kompozycja dwóch aplikantów jest z konieczności aplikatywna. Podejrzewam, że intencją pierwotnego stwierdzenia było to, że „Aplikacyjność komponuje, a monadność nie”. Ponownie, „ Applicativejest zamknięty w kompozycji, a Monadnie jest”.

Conal
źródło
24
Dodatkowo dowolne dwa zastosowania komponują się w sposób całkowicie mechaniczny, podczas gdy monada utworzona przez kompozycję dwóch monad jest specyficzna dla tej kompozycji.
Apocalisp
12
Co więcej, monady składają się w inny sposób, produkt dwóch monad jest monadą, tylko koprodukty wymagają pewnego rodzaju prawa dystrybucji.
Edward KMETT,
Uwzględniając komentarz @Apocalisp, jest to najlepsza i najbardziej zwięzła odpowiedź.
Paul Draper
39

Jeśli masz aplikacje A1i A2, to typ data A3 a = A3 (A1 (A2 a))też jest aplikacyjny (możesz napisać taką instancję w sposób ogólny).

Z drugiej strony, jeśli masz monady M1i M2typ data M3 a = M3 (M1 (M2 a))niekoniecznie jest monadą (nie ma sensownej ogólnej implementacji dla >>=lub joindla kompozycji).

Jednym z przykładów może być typu [Int -> a](tu skomponować konstruktora typu []z (->) Int, z których oba są monady). Możesz łatwo pisać

app :: [Int -> (a -> b)] -> [Int -> a] -> [Int -> b]
app f x = (<*>) <$> f <*> x

I to uogólnia na każdą aplikację:

app :: (Applicative f, Applicative f1) => f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)

Ale nie ma sensownej definicji

join :: [Int -> [Int -> a]] -> [Int -> a]

Jeśli nie jesteś tego przekonany, rozważ następujące wyrażenie:

join [\x -> replicate x (const ())]

Długość zwracanej listy musi być ustawiona w kamieniu przed podaniem liczby całkowitej, ale prawidłowa jej długość zależy od podanej liczby całkowitej. Dlatego nie joinmoże istnieć żadna poprawna funkcja dla tego typu.

Rotsor
źródło
1
... więc unikaj monad, gdy funkcja będzie działać?
andrew cooke
2
@andrew, jeśli chodziło Ci o funktor, to tak, funktory są prostsze i powinny być używane, gdy są wystarczające. Pamiętaj, że nie zawsze. Na przykład IObez programu Monadbyłoby bardzo trudne do zaprogramowania. :)
Rotsor
17

Niestety nasz prawdziwy cel, czyli kompozycja monad, jest raczej trudniejszy. .. W rzeczywistości możemy faktycznie udowodnić, że w pewnym sensie nie ma sposobu, aby skonstruować funkcję złączenia z powyższym typem, używając tylko operacji dwóch monad (patrz załącznik, aby zapoznać się z zarysem dowodu). Wynika z tego, że jedyny sposób, w jaki możemy mieć nadzieję na utworzenie kompozycji, to istnienie dodatkowych konstrukcji łączących te dwa składniki.

Komponowanie monad, http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf

Landei
źródło
4
Tl; dr dla niecierpliwych czytelników: możesz komponować monady, jeśli (f?) Potrafisz zapewnić naturalną transformacjęswap : N M a -> M N a
Alexandre C.
@Alexandre C .: Podejrzewam, że tylko „jeśli”. Nie wszystkie transformatory monadowe są opisane przez bezpośredni skład funktorów. Na przykład ContT r m anie jest ani m (Cont r a)ani Cont r (m a), i StateT s m ajest z grubsza Reader s (m (Writer s a)).
CA McCann
@CA McCann: Nie mogę przejść z (M monada, N monada, MN monada, NM monada) do (istnieje zamiana: MN -> NM naturalny). Więc na razie trzymajmy się „jeśli” (być może odpowiedź jest w gazecie, muszę przyznać, że szybko ją sprawdziłem)
Alexandre C.
1
@Alexandre C .: Samo określenie, że kompozycje są monadami, i tak może nie wystarczyć - trzeba też jakoś powiązać obie części z całością. Istnienie swapimplikuje, że kompozycja pozwala jakoś „współpracować”. Zauważ również, że sequencejest to specjalny przypadek „zamiany” dla niektórych monad. Tak jest flipw rzeczywistości.
CA McCann,
7
Aby napisać swap :: N (M x) -> M (N x), wygląda mi na to, że możesz użyć returns(odpowiednio fmapped), aby wstawić Mz przodu i Nz tyłu, przechodząc od N (M x) -> M (N (M (N x))), a następnie użyj joinkompozytu, aby uzyskać swój M (N x).
pigworker
7

Rozwiązanie prawa dystrybucji l: MN -> NM jest wystarczające

aby zagwarantować monadyczność NM. Aby to zobaczyć, potrzebujesz jednostki i wielu. skoncentruję się na mnogości (jednostka to jednostka_N jednostka M)

NMNM - l -> NNMM - mult_N mult_M -> NM

Ten sposób nie gwarantuje, że MN jest monada.

Kluczowa obserwacja pojawia się jednak, gdy masz rozwiązania prawa dystrybucyjnego

l1 : ML -> LM
l2 : NL -> LN
l3 : NM -> MN

zatem LM, LN i MN są monadami. Powstaje pytanie, czy LMN jest monadą (albo wg

(MN) L -> L (MN) lub przez N (LM) -> (LM) N

Mamy wystarczającą strukturę, aby stworzyć te mapy. Jednak, jak zauważa Eugenia Cheng , potrzebujemy warunku heksagonalnego (który sprowadza się do przedstawienia równania Yanga-Baxtera), aby zagwarantować monadyczność obu konstrukcji. W rzeczywistości, przy warunku heksagonalnym, dwie różne monady pokrywają się.

user278559
źródło
9
Prawdopodobnie jest to świetne rozwiązanie, ale to poszło Whoosh drogę nad moją głową.
Dan Burton
1
Dzieje się tak, ponieważ używając terminu Applicative i znacznika haskell, mamy do czynienia z pytaniem o haskell, ale z odpowiedzią w innym zapisie.
Codeshot