Warum brauchen wir Monaden?

366

Meiner bescheidenen Meinung nach die Antworten auf die berühmte Frage "Was ist eine Monade?" Versuchen Sie, insbesondere die am häufigsten gewählten, zu erklären, was eine Monade ist, ohne klar zu erklären, warum Monaden wirklich notwendig sind . Können sie als Lösung für ein Problem erklärt werden?

cibercitizen1
quelle
4
Welche Recherchen haben Sie bereits durchgeführt? Wo hast du gesucht Welche Ressourcen haben Sie gefunden? Wir erwarten, dass Sie vor dem Fragen eine erhebliche Menge an Recherchen durchführen und uns in der Frage zeigen, welche Recherchen Sie durchgeführt haben . Es gibt viele Ressourcen, die versuchen, die Motivation für Ressourcen zu erklären. Wenn Sie überhaupt keine gefunden haben, müssen Sie möglicherweise etwas mehr Nachforschungen anstellen. Wenn Sie einige gefunden haben, diese Ihnen aber nicht geholfen haben, wäre dies eine bessere Frage, wenn Sie erklären würden, was Sie gefunden haben und warum sie speziell für Sie nicht funktionieren.
DW
8
Dies passt definitiv besser zu Programmers.StackExchange und nicht zu StackOverflow. Ich würde für eine Migration stimmen, wenn ich könnte, aber ich kann nicht. = (
jpmc26
3
@ jpmc26 Höchstwahrscheinlich würde es dort als "hauptsächlich meinungsbasiert" geschlossen werden; hier hat es zumindest eine Chance (wie die große Anzahl von Upvotes zeigt, die schnelle Wiedereröffnung gestern und noch keine engen Stimmen mehr)
Izkata

Antworten:

580

Warum brauchen wir Monaden?

  1. Wir wollen nur mit Funktionen programmieren . (Immerhin "funktionale Programmierung (FP)").
  2. Dann haben wir ein erstes großes Problem. Dies ist ein Programm:

    f(x) = 2 * x

    g(x,y) = x / y

    Wie können wir sagen, was zuerst ausgeführt werden soll ? Wie können wir eine geordnete Folge von Funktionen (dh ein Programm ) mit nur Funktionen bilden ?

    Lösung: Funktionen erstellen . Wenn Sie zuerst gund dann wollen f, schreiben Sie einfach f(g(x,y)). Auf diese Weise ist "das Programm" auch eine Funktion : main = f(g(x,y)). OK aber ...

  3. Weitere Probleme: Einige Funktionen können fehlschlagen (dh g(2,0)durch 0 teilen). Wir haben keine "Ausnahmen" in FP (eine Ausnahme ist keine Funktion). Wie lösen wir das?

    Lösung: Lassen Sie zu, dass Funktionen zwei Arten von Dingen zurückgeben : Anstatt g : Real,Real -> Real(Funktion von zwei Real in ein Real) zu haben, lassen Sie uns g : Real,Real -> Real | Nothing(Funktion von zwei Real in (Real oder nichts)) zulassen .

  4. Aber Funktionen sollten (um einfacher zu sein) nur eines zurückgeben .

    Lösung: Lassen Sie uns einen neuen Datentyp erstellen, der zurückgegeben werden soll, einen " Boxtyp ", der möglicherweise einen echten oder einfach nichts enthält. Daher können wir haben g : Real,Real -> Maybe Real. OK aber ...

  5. Was passiert jetzt mit f(g(x,y))? fist nicht bereit zu konsumieren a Maybe Real. Und wir möchten nicht jede Funktion ändern, mit der wir uns verbinden können g, um a zu konsumieren Maybe Real.

    Lösung: Lassen Sie uns eine spezielle Funktion zum "Verbinden" / "Verfassen" / "Verknüpfen" -Funktionen haben . Auf diese Weise können wir hinter den Kulissen die Ausgabe einer Funktion anpassen, um die folgende zu speisen.

    In unserem Fall: g >>= f(verbinden / komponieren gmit f). Wir möchten >>=die gAusgabe Nothingabrufen , sie überprüfen und, falls dies nicht der Fall ist , nicht anrufen fund zurückgeben Nothing. oder im Gegenteil, extrahieren Sie die Box Realund füttern fSie damit. (Dieser Algorithmus ist nur die Implementierung >>=für den MaybeTyp). Beachten Sie auch, dass nur einmal pro "Boxtyp" >>=geschrieben werden darf (unterschiedliche Box, unterschiedlicher Anpassungsalgorithmus).

  6. Es treten viele andere Probleme auf, die mit demselben Muster gelöst werden können: 1. Verwenden Sie eine "Box", um verschiedene Bedeutungen / Werte zu codieren / zu speichern, und lassen Sie gsolche Funktionen diese "Box-Werte" zurückgeben. 2. Haben Sie einen Komponisten / Linker g >>= f, der Ihnen hilft g, den Ausgang mit fdem Eingang zu verbinden, damit wir überhaupt nichts ändern müssen f.

  7. Bemerkenswerte Probleme, die mit dieser Technik gelöst werden können, sind:

    • einen globalen Zustand haben, den jede Funktion in der Folge von Funktionen ("das Programm") gemeinsam nutzen kann: Lösung StateMonad.

    • Wir mögen keine "unreinen Funktionen": Funktionen, die für dieselbe Eingabe unterschiedliche Ausgaben liefern . Markieren wir diese Funktionen daher so, dass sie einen Wert mit Tags / Boxen zurückgeben: monad.IO

Totales Glück!

cibercitizen1
quelle
64
@ Carl Bitte schreiben Sie eine bessere Antwort, um uns aufzuklären
XrXr
15
@ Carl Ich denke, dass es in der Antwort klar ist, dass es viele, viele Probleme gibt, die von diesem Muster profitieren (Punkt 6) und dass IOMonade nur ein weiteres Problem in der Liste ist IO(Punkt 7). Auf der anderen Seite IOerscheint nur einmal und am Ende, also verstehe nicht, dass du "die meiste Zeit ... über IO sprichst".
Cibercitizen1
4
Die großen Missverständnisse über Monaden: Monaden über den Staat; Monaden über Ausnahmebehandlung; Es gibt keine Möglichkeit, E / A in reinen FPL ohne Monaden zu implementieren. Monaden sind eindeutig (Gegenargument ist Either). Die meisten Antworten beziehen sich auf "Warum brauchen wir Funktoren?".
Vlastachu
4
"6. 2. Haben Sie einen Komponisten / Linker g >>= f, der Ihnen hilft g, den Ausgang mit fdem Eingang zu verbinden, damit wir überhaupt nichts ändern müssen f." das ist überhaupt nicht richtig . Bevor, in f(g(x,y)), fkonnte alles produzieren. Es könnte sein f:: Real -> String. Bei "monadischer Komposition" muss es geändert werden , um zu produzieren Maybe String, sonst passen die Typen nicht. Außerdem passt>>= selbst nicht !! Es ist das >=>, was diese Komposition macht, nicht >>=. Siehe die Diskussion mit dfeuer unter Carls Antwort.
Will Ness
3
Ihre Antwort ist in dem Sinne richtig, dass Monaden IMO in der Tat am besten als Zusammensetzung / Alität von "Funktionen" beschrieben werden (Kleisli-Pfeile wirklich), aber die genauen Details darüber, welcher Typ wohin geht, machen sie zu "Monaden". Sie können die Boxen auf alle Arten verkabeln (wie Functor usw.). Diese spezielle Art, sie miteinander zu verbinden, definiert "die Monade".
Will Ness
219

Die Antwort lautet natürlich "Wir nicht" . Wie bei allen Abstraktionen ist dies nicht erforderlich.

Haskell braucht keine Monadenabstraktion. Es ist nicht erforderlich, E / A in einer reinen Sprache auszuführen. Der IOTyp kümmert sich gut darum. Die bestehende monadischen Entzuckerung von doBlöcken könnte mit Entzuckerung ersetzt werden bindIO, returnIOund , failIOwie in der definierten GHC.BaseModul. (Es handelt sich nicht um ein dokumentiertes Modul zum Thema Hackage, daher muss ich zur Dokumentation auf seine Quelle verweisen .) Nein, die Monadenabstraktion ist nicht erforderlich.

Also, wenn es nicht benötigt wird, warum existiert es? Weil festgestellt wurde, dass viele Berechnungsmuster monadische Strukturen bilden. Die Abstraktion einer Struktur ermöglicht das Schreiben von Code, der über alle Instanzen dieser Struktur hinweg funktioniert. Genauer gesagt - Wiederverwendung von Code.

In funktionalen Sprachen war die Zusammenstellung von Funktionen das leistungsstärkste Werkzeug für die Wiederverwendung von Code. Der gute alte (.) :: (b -> c) -> (a -> b) -> (a -> c)Bediener ist außerordentlich mächtig. Es macht es einfach, winzige Funktionen zu schreiben und sie mit minimalem syntaktischen oder semantischen Aufwand zusammenzukleben.

Es gibt jedoch Fälle, in denen die Typen nicht ganz richtig funktionieren. Was machst du wenn du hast foo :: (b -> Maybe c)und bar :: (a -> Maybe b)? foo . bartypecheck nicht, weil bund Maybe bnicht der gleiche Typ.

Aber ... es ist fast richtig. Sie wollen nur ein bisschen Spielraum. Sie möchten in der Lage sein, so zu behandeln, Maybe bals ob es im Grunde wäre b. Es ist jedoch eine schlechte Idee, sie einfach als den gleichen Typ zu behandeln. Das ist mehr oder weniger dasselbe wie Nullzeiger, die Tony Hoare berühmt als Milliarden-Dollar-Fehler bezeichnete . Wenn Sie sie also nicht als denselben Typ behandeln können, können Sie möglicherweise einen Weg finden, um den Kompositionsmechanismus zu erweitern (.).

In diesem Fall ist es wichtig, die zugrunde liegende Theorie wirklich zu untersuchen (.). Zum Glück hat das schon jemand für uns gemacht. Es stellt sich heraus, dass die Kombination (.)und idBildung eines mathematischen Konstrukts als Kategorie bekannt ist . Es gibt aber auch andere Möglichkeiten, Kategorien zu bilden. Eine Kleisli-Kategorie ermöglicht es beispielsweise, die zusammengesetzten Objekte ein wenig zu erweitern. Eine Kleisli-Kategorie für Maybewürde aus (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)und bestehen id :: a -> Maybe a. Das heißt, die Objekte in der Kategorie ergänzen das (->)mit a Maybe, so (a -> b)wird (a -> Maybe b).

Und plötzlich haben wir die Kraft der Komposition auf Dinge ausgedehnt, an denen die traditionelle (.)Operation nicht funktioniert. Dies ist eine Quelle neuer Abstraktionskraft. Kleisli-Kategorien arbeiten mit mehr Typen als nur Maybe. Sie arbeiten mit jedem Typ, der eine richtige Kategorie zusammenstellen kann, wobei die Kategoriengesetze eingehalten werden.

  1. Linke Identität: id . f=f
  2. Richtige Identität: f . id=f
  3. Assoziativität: f . (g . h)=(f . g) . h

Solange Sie nachweisen können, dass Ihr Typ diese drei Gesetze befolgt, können Sie ihn in eine Kleisli-Kategorie umwandeln. Und was ist das große daran? Nun, es stellt sich heraus, dass Monaden genau dasselbe sind wie Kleisli-Kategorien. Monad‚s returnist die gleiche wie Kleisli id. Monad‚s (>>=)ist nicht identisch mit Kleisli (.), aber es stellt sich heraus , sehr einfach zu schreiben jeweils in Bezug auf den anderen. Und die Kategoriengesetze sind die gleichen wie die Monadengesetze, wenn Sie sie über den Unterschied zwischen (>>=)und hinweg übersetzen (.).

Warum also all diese Mühe machen? Warum eine MonadAbstraktion in der Sprache? Wie ich oben angedeutet habe, ermöglicht es die Wiederverwendung von Code. Es ermöglicht sogar die Wiederverwendung von Code in zwei verschiedenen Dimensionen.

Die erste Dimension der Wiederverwendung von Code ergibt sich direkt aus dem Vorhandensein der Abstraktion. Sie können Code schreiben, der für alle Instanzen der Abstraktion funktioniert. Es gibt das gesamte Monad-Loops- Paket, das aus Loops besteht, die mit jeder Instanz von funktionieren Monad.

Die zweite Dimension ist indirekt, folgt aber aus der Existenz der Komposition. Wenn die Komposition einfach ist, ist es natürlich, Code in kleinen, wiederverwendbaren Blöcken zu schreiben. Auf die gleiche Weise (.)fördert der Operator für Funktionen das Schreiben kleiner, wiederverwendbarer Funktionen.

Warum existiert die Abstraktion? Weil es sich als ein Tool erwiesen hat, das mehr Komposition im Code ermöglicht, was zur Erstellung von wiederverwendbarem Code und zur Förderung der Erstellung von wiederverwendbarem Code führt. Die Wiederverwendung von Code ist einer der heiligen Grale der Programmierung. Die Monadenabstraktion existiert, weil sie uns ein wenig in Richtung dieses heiligen Grals bewegt.

Carl
quelle
2
Können Sie die Beziehung zwischen Kategorien allgemein und Kleisli-Kategorien erklären? Die drei Gesetze, die Sie beschreiben, gelten für jede Kategorie.
dfeuer
1
@dfeuer Oh. Um es in Code zu setzen , newtype Kleisli m a b = Kleisli (a -> m b). Kleisli-Kategorien sind Funktionen, bei denen der kategoriale Rückgabetyp ( bin diesem Fall) das Argument für einen Typkonstruktor ist m. Iff Kleisli mbildet eine Kategorie, mist eine Monade.
Carl
1
Was genau ist ein kategorialer Rückgabetyp? Kleisli mscheint eine Kategorie zu bilden, deren Objekte Haskell-Typen sind und deren Pfeile von abis bdie Funktionen von abis m b, mit id = returnund sind (.) = (<=<). Ist das ungefähr richtig oder vermische ich verschiedene Ebenen von Dingen oder so?
Feuer
1
@dfeuer Das stimmt. Die Objekte sind alle Typen, und die Morphismen liegen zwischen den Typen aund b, aber sie sind keine einfachen Funktionen. Sie sind mit einem zusätzlichen mRückgabewert der Funktion versehen.
Carl
1
Wird die Terminologie der Kategorietheorie wirklich benötigt? Vielleicht wäre Haskell einfacher, wenn Sie die Typen in Bilder verwandeln würden, wobei der Typ die DNA für das Zeichnen der Bilder wäre (obwohl abhängige Typen *), und Sie dann das Bild verwenden, um Ihr Programm mit den Namen zu schreiben, die kleine Rubinzeichen sind über dem Symbol.
aoeu256
24

Benjamin Pierce sagte in TAPL

Ein Typsystem kann als Berechnung einer Art statischer Annäherung an das Laufzeitverhalten der Begriffe in einem Programm angesehen werden.

Deshalb ist eine Sprache, die mit einem leistungsfähigen Schriftsystem ausgestattet ist, streng ausdrucksstärker als eine schlecht typisierte Sprache. Sie können genauso über Monaden denken.

Als @Carl- und Sigfpe- Punkt können Sie einen Datentyp mit allen gewünschten Operationen ausstatten, ohne auf Monaden, Typklassen oder andere abstrakte Dinge zurückgreifen zu müssen. Mit Monaden können Sie jedoch nicht nur wiederverwendbaren Code schreiben, sondern auch alle redundanten Details abstrahieren.

Angenommen, wir möchten eine Liste filtern. Der einfachste Weg ist die Verwendung der filterFunktion : filter (> 3) [1..10], die gleich ist [4,5,6,7,8,9,10].

Eine etwas kompliziertere Version von filter, die auch einen Akkumulator von links nach rechts durchläuft, ist

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

Um alles iso zu bekommen , dass i <= 10, sum [1..i] > 4, sum [1..i] < 25wir schreiben können

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

was gleich ist [3,4,5,6].

Oder wir können die nubFunktion, mit der doppelte Elemente aus einer Liste entfernt werden, neu definieren filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4]gleich [1,2,4,5,3,8,9]. Hier wird eine Liste als Akkumulator übergeben. Der Code funktioniert, weil es möglich ist, die Listenmonade zu verlassen, sodass die gesamte Berechnung rein bleibt (wird notElemeigentlich nicht verwendet >>=, könnte es aber). Es ist jedoch nicht möglich, die E / A-Monade sicher zu verlassen (dh Sie können keine E / A-Aktion ausführen und einen reinen Wert zurückgeben - der Wert wird immer in die E / A-Monade eingeschlossen). Ein weiteres Beispiel sind veränderbare Arrays: Nachdem Sie die ST-Monade verlassen haben, in der sich ein veränderliches Array befindet, können Sie das Array nicht mehr in konstanter Zeit aktualisieren. Wir brauchen also eine monadische Filterung aus dem Control.MonadModul:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterMführt eine monadische Aktion für alle Elemente aus einer Liste aus und ergibt Elemente, für die die monadische Aktion zurückgegeben wird True.

Ein Filterbeispiel mit einem Array:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

druckt [1,2,4,5,3,8,9]wie erwartet.

Und eine Version mit der IO-Monade, in der gefragt wird, welche Elemente zurückgegeben werden sollen:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

Z.B

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

Und als letzte Illustration filterAccumkann definiert werden in Bezug auf filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

Die StateTMonade, die unter der Haube verwendet wird, ist nur ein gewöhnlicher Datentyp.

Dieses Beispiel zeigt, dass Sie mit Monaden nicht nur den Rechenkontext abstrahieren und sauberen wiederverwendbaren Code schreiben können (aufgrund der Zusammensetzbarkeit von Monaden, wie @Carl erklärt), sondern auch benutzerdefinierte Datentypen und integrierte Grundelemente einheitlich behandeln können.

user3237465
quelle
1
Diese Antwort erklärt, warum wir die Monad-Typklasse benötigen. Der beste Weg zu verstehen, warum wir Monaden brauchen und nicht etwas anderes, ist über den Unterschied zwischen Monaden und anwendenden Funktoren zu lesen: eins , zwei .
user3237465
20

Ich denke nicht, dass IOes als besonders herausragende Monade angesehen werden sollte, aber es ist sicherlich eine der erstaunlichsten für Anfänger, also werde ich es für meine Erklärung verwenden.

Naiv ein IO-System für Haskell bauen

Das einfachste denkbare E / A-System für eine rein funktionale Sprache (und tatsächlich das, mit dem Haskell begonnen hat) ist folgendes:

main :: String -> String
main _ = "Hello World"

Mit Faulheit reicht diese einfache Signatur aus, um tatsächlich interaktive Terminalprogramme zu erstellen - allerdings sehr begrenzt. Am frustrierendsten ist, dass wir nur Text ausgeben können. Was wäre, wenn wir weitere aufregende Ausgabemöglichkeiten hinzufügen würden?

data Output = TxtOutput String
            | Beep Frequency

main :: String -> [Output]
main _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

süß, aber natürlich wäre eine viel realistischere "alternative Ausgabe" das Schreiben in eine Datei . Aber dann möchten Sie auch eine Möglichkeit, aus Dateien zu lesen . Irgendeine Chance?

Nun, wenn wir unser main₁Programm nehmen und einfach eine Datei an den Prozess weiterleiten (unter Verwendung von Betriebssystemfunktionen), haben wir im Wesentlichen das Lesen von Dateien implementiert. Wenn wir das Lesen von Dateien aus der Haskell-Sprache heraus auslösen könnten ...

readFile :: Filepath -> (String -> [Output]) -> [Output]

Dies würde ein "interaktives Programm" verwenden String->[Output], ihm eine aus einer Datei erhaltene Zeichenfolge zuführen und ein nicht interaktives Programm ergeben, das einfach das angegebene Programm ausführt.

Hier gibt es ein Problem: Wir wissen nicht genau, wann die Datei gelesen wird. Die [Output]Liste gibt den Ausgaben sicher eine schöne Reihenfolge , aber wir bekommen keine Reihenfolge, wann die Eingaben erfolgen werden.

Lösung: Geben Sie Eingabeereignisse auch in die Liste der zu erledigenden Aufgaben ein.

data IO = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main :: String -> [IO₀]
main _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

Ok, jetzt können Sie ein Ungleichgewicht feststellen: Sie können eine Datei lesen und die Ausgabe davon abhängig machen, aber Sie können den Dateiinhalt nicht verwenden, um zu entscheiden, z. B. auch eine andere Datei zu lesen. Offensichtliche Lösung: Machen Sie das Ergebnis der Eingabeereignisse auch zu etwas Typischem IO, nicht nur Output. Das beinhaltet sicher eine einfache Textausgabe, ermöglicht aber auch das Lesen zusätzlicher Dateien usw.

data IO = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main :: String -> [IO₁]
main _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

Das würde es Ihnen jetzt tatsächlich ermöglichen, jede gewünschte Dateivorgang in einem Programm auszudrücken (wenn auch möglicherweise nicht mit guter Leistung), aber es ist etwas überkompliziert:

  • main₃ergibt eine ganze Liste von Aktionen. Warum verwenden wir nicht einfach die Signatur :: IO₁, die dies als Sonderfall hat?

  • Die Listen geben keinen verlässlichen Überblick mehr über den Programmablauf: Die meisten nachfolgenden Berechnungen werden nur als Ergebnis einer Eingabeoperation "angekündigt". Wir könnten also genauso gut die Listenstruktur fallen lassen und einfach ein "und dann" für jede Ausgabeoperation tun.

data IO = TxtOut String IO
         | TxtIn (String -> IO₂)
         | Terminate

main :: IO
main = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

Nicht so schlecht!

Was hat das alles mit Monaden zu tun?

In der Praxis möchten Sie keine einfachen Konstruktoren verwenden, um alle Ihre Programme zu definieren. Es müsste ein paar solcher grundlegender Konstruktoren geben, aber für die meisten übergeordneten Dinge möchten wir eine Funktion mit einer schönen Signatur auf hoher Ebene schreiben. Es stellt sich heraus, dass die meisten davon ziemlich ähnlich aussehen würden: Akzeptieren Sie einen sinnvoll typisierten Wert und führen Sie als Ergebnis eine E / A-Aktion aus.

getTime :: (UTCTime -> IO₂) -> IO
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO

Hier gibt es offensichtlich ein Muster, und wir sollten es besser so schreiben

type IO a = (a -> IO₂) -> IO    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO UTCTime
randomRIO :: Random r => (r,r) -> IO r
findFile :: RegEx -> IO (Maybe FilePath)

Das kommt mir jetzt bekannt vor, aber wir haben es immer noch nur mit dünn getarnten einfachen Funktionen unter der Haube zu tun, und das ist riskant: Jede „Wertaktion“ hat die Verantwortung, die resultierende Aktion einer enthaltenen Funktion tatsächlich weiterzugeben (sonst Der Kontrollfluss des gesamten Programms kann leicht durch eine schlecht benommene Aktion in der Mitte unterbrochen werden. Wir sollten diese Anforderung besser explizit machen. Nun, es stellt sich heraus, dass dies die Monadengesetze sind , obwohl ich nicht sicher bin, ob wir sie wirklich ohne die Standard-Bind / Join-Operatoren formulieren können.

Auf jeden Fall haben wir jetzt eine Formulierung von IO erreicht, die eine richtige Monadeninstanz hat:

data IO a = TxtOut String (IO a)
           | TxtIn (String -> IO a)
           | TerminateWith a

txtOut :: String -> IO ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO String
txtIn = TxtIn $ TerminateWith

instance Functor IO where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

Dies ist natürlich keine effiziente Implementierung von IO, aber im Prinzip verwendbar.

links herum
quelle
@jdlugosz : IO3 a ≡ Cont IO2 a. Aber ich meinte diesen Kommentar eher als Anspielung auf diejenigen, die die Fortsetzung Monade bereits kennen, da sie nicht gerade den Ruf hat, anfängerfreundlich zu sein.
links um den
4

Monaden sind nur ein praktischer Rahmen für die Lösung einer Klasse wiederkehrender Probleme. Erstens müssen Monaden Funktoren sein (dh müssen die Zuordnung unterstützen, ohne die Elemente (oder ihren Typ) zu betrachten), sie müssen auch eine Bindungs- (oder Verkettungs-) Operation und eine Möglichkeit zum Erstellen eines monadischen Werts aus einem Elementtyp ( return) enthalten. Schließlich bindund returnmuss zwei Gleichungen (linke und rechte Identität) erfüllen, die auch als Monadengesetze bezeichnet werden. (Alternativ könnte man Monaden so definieren, dass sie eine flattening operationanstelle einer Bindung haben.)

Die Listenmonade wird üblicherweise verwendet, um mit Nichtdeterminismus umzugehen. Die Bindeoperation wählt ein Element der Liste aus (intuitiv alle in parallelen Welten ), lässt den Programmierer einige Berechnungen mit ihnen durchführen und kombiniert dann die Ergebnisse in allen Welten zu einer einzigen Liste (durch Verketten oder Reduzieren einer verschachtelten Liste) ). So würde man eine Permutationsfunktion im monadischen Rahmen von Haskell definieren:

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

Hier ist ein Beispiel für eine Repl- Sitzung:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

Es ist zu beachten, dass die Listenmonade in keiner Weise eine Nebeneffektberechnung darstellt. Eine mathematische Struktur, die eine Monade ist (dh den oben genannten Schnittstellen und Gesetzen entspricht), impliziert keine Nebenwirkungen, obwohl Nebeneffekte oft gut in das monadische Gerüst passen.

Heisenbug
quelle
3

Monaden dienen im Wesentlichen dazu, Funktionen in einer Kette zusammenzusetzen. Zeitraum.

Die Art und Weise, wie sie sich zusammensetzen, unterscheidet sich nun zwischen den vorhandenen Monaden, was zu unterschiedlichen Verhaltensweisen führt (z. B. um einen veränderlichen Zustand in der Zustandsmonade zu simulieren).

Die Verwirrung über Monaden besteht darin, dass sie so allgemein sind, dh ein Mechanismus zum Zusammenstellen von Funktionen, dass sie für viele Dinge verwendet werden können, was die Leute glauben lässt, dass es bei Monaden um den Zustand, um E / A usw. geht, wenn es nur um das "Zusammensetzen von Funktionen" geht ".

Eine interessante Sache bei Monaden ist, dass das Ergebnis der Komposition immer vom Typ "M a" ist, dh ein Wert in einem Umschlag, der mit "M" gekennzeichnet ist. Diese Funktion ist wirklich nett, um beispielsweise eine klare Trennung zwischen reinem und unreinem Code zu implementieren: Deklarieren Sie alle unreinen Aktionen als Funktionen vom Typ "IO a" und geben Sie beim Definieren der IO-Monade keine Funktion zum Herausnehmen der " ein "Wert aus dem" IO a ". Das Ergebnis ist, dass keine Funktion rein sein kann und gleichzeitig einen Wert aus einem "IO a" herausnimmt, da es keine Möglichkeit gibt, einen solchen Wert anzunehmen, während sie rein bleibt (die Funktion muss sich innerhalb der zu verwendenden "IO" -Monade befinden ein solcher Wert). (HINWEIS: Nun, nichts ist perfekt, daher kann die "IO-Zwangsjacke" mit "unsafePerformIO: IO a -> a" beschädigt werden.

mljrg
quelle
2

Sie benötigen Monaden, wenn Sie einen Typkonstruktor und Funktionen haben, die Werte dieser Typfamilie zurückgeben . Schließlich möchten Sie diese Art von Funktionen miteinander kombinieren . Dies sind die drei Schlüsselelemente, um zu beantworten, warum .

Lassen Sie mich näher darauf eingehen. Sie haben Int, Stringund Realund Funktionen des Typs Int -> String, String -> Realund so weiter. Sie können diese Funktionen einfach kombinieren und mit enden Int -> Real. Das leben ist gut.

Dann, eines Tages, müssen Sie eine erstellen neue Familie von Typen . Dies kann daran liegen, dass Sie die Möglichkeit in Betracht ziehen müssen, keinen Wert ( Maybe), einen Fehler ( Either), mehrere Ergebnisse ( List) usw. zurückzugeben.

Beachten Sie, dass dies Maybeein Typkonstruktor ist. Es nimmt einen Typ wie an Intund gibt einen neuen Typ zurück Maybe Int. Das erste, woran man sich erinnern sollte, kein Typkonstruktor, keine Monade.

Natürlich möchten Sie Ihren Typkonstruktor in Ihrem Code verwenden, und bald enden Sie mit Funktionen wie Int -> Maybe Stringund String -> Maybe Float. Jetzt können Sie Ihre Funktionen nicht einfach kombinieren. Das Leben ist nicht mehr gut.

Und hier kommen Monaden zur Rettung. Mit ihnen können Sie diese Art von Funktionen wieder kombinieren. Sie müssen nur die Zusammensetzung ändern . für > == .

jdinunzio
quelle
2
Dies hat nichts mit Typfamilien zu tun. Worüber sprichst du eigentlich?
Feuer