web-dev-qa-db-ger.com

Warum brauchen wir Monaden?

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

349
cibercitizen1

Warum brauchen wir Monaden?

  1. Wir wollen nur mit Funktionen programmieren. (Immerhin "Functional Programming (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 soll zuerst ausgeführt werden? Wie können wir eine geordnete Folge von Funktionen bilden (d. H. ein Programm) , wobei wir nicht mehr als Funktionen verwenden?

    Lösung: Funktionen erstellen. Wenn Sie zuerst g und dann f möchten, 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önnten fehlschlagen (d. H. g(2,0), dividieren durch 0). Wir haben keine "Ausnahmen" in FP (eine Ausnahme ist keine Funktion). Wie lösen wir sie?

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

  4. Funktionen sollten aber (um einfacher zu sein) nur eins zurückgeben.

    Lösung: Erstellen wir einen neuen Datentyp, der zurückgegeben werden soll, einen " boxing type", der möglicherweise ein reales oder einfach nichts enthält. Daher können wir g : Real,Real -> Maybe Real Haben. OK aber ...

  5. Was passiert jetzt mit f(g(x,y))? f ist nicht bereit, einen Maybe Real Zu verbrauchen. Und wir möchten nicht jede Funktion ändern, die wir mit g verbinden könnten, um einen Maybe Real Zu verbrauchen.

    Lösung: Lassen Sie uns eine spezielle Funktion zum "Verbinden"/"Verfassen"/"Verknüpfen" von 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 Sie g mit f). Wir möchten, dass >>= Die Ausgabe von g erhält, sie überprüft und, falls es sich um Nothing handelt, einfach nicht f aufruft und Nothing; oder im Gegenteil, extrahieren Sie die Box Real und füttern Sie f damit. (Dieser Algorithmus ist nur die Implementierung von >>= Für den Typ Maybe). Beachten Sie auch, dass >>= nur einmal pro "Boxtyp" geschrieben werden muss (anderes Feld, anderer 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 haben Sie Funktionen wie g, die diese "Boxed Values" zurückgeben. 2. Haben Sie einen Komponisten/Linker g >>= f, Der Ihnen hilft, die Ausgabe von g mit der Eingabe von f zu verbinden, damit wir keine Änderung von f vornehmen müssen. ] überhaupt.

  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") teilen kann: Lösung StateMonad.

    • Wir mögen keine "unreinen Funktionen": Funktionen, die unterschiedliche Ausgaben für gleiche Eingaben liefern. Markieren Sie daher diese Funktionen, damit sie einen getaggten Wert zurückgeben: IO monad.

Völliges Glück!

551
cibercitizen1

Die Antwort lautet natürlich "Wir nicht" . Wie bei allen Abstraktionen ist es nicht notwendig.

Haskell braucht keine Monadenabstraktion. Es ist nicht notwendig, IO in einer reinen Sprache auszuführen. Der Typ IO erledigt das von selbst. Die vorhandene monadische Desugarierung von do Blöcke können durch Desugaring in bindIO, returnIO und failIO ersetzt werden, wie im Modul GHC.Base definiert Ich muss zur Dokumentation auf seine Quelle verweisen.) Nein, die Monadenabstraktion ist also nicht erforderlich.

Also, wenn es nicht benötigt wird, warum gibt es es? Weil festgestellt wurde, dass viele Rechenmuster monadische Strukturen bilden. Die Abstraktion einer Struktur ermöglicht das Schreiben von Code, der in allen Instanzen dieser Struktur funktioniert. Um es kurz zu machen: Wiederverwendung von Code.

In funktionalen Sprachen war die Komposition von Funktionen das mächtigste Werkzeug für die Wiederverwendung von Code. Der gute alte Operator (.) :: (b -> c) -> (a -> b) -> (a -> c) Ist überaus mächtig. Es macht es einfach, winzige Funktionen zu schreiben und diese mit minimalem syntaktischen oder semantischen Aufwand zusammenzufügen.

Aber es gibt Fälle, in denen die Typen nicht ganz richtig funktionieren. Was machst du, wenn du foo :: (b -> Maybe c) und bar :: (a -> Maybe b) hast? foo . bar Prüft nicht, da b und Maybe b Nicht der gleiche Typ sind.

Aber ... es ist fast richtig. Du willst nur ein bisschen Spielraum. Sie möchten in der Lage sein, Maybe b So zu behandeln, als wäre es im Grunde b. Es ist jedoch eine schlechte Idee, sie einfach als denselben Typ zu behandeln. Das ist mehr oder weniger dasselbe wie Null-Zeiger, die Tony Hoare bekanntermaßen der Milliarden-Dollar-Fehler nannte. Wenn Sie sie also nicht als denselben Typ behandeln können, finden Sie möglicherweise eine Möglichkeit, 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 von (.) Und id ein mathematisches Konstrukt bildet, das als Kategorie bekannt ist. Es gibt aber auch andere Möglichkeiten, Kategorien zu bilden. Eine Kleisli-Kategorie zum Beispiel ermöglicht es, die zu komponierenden Objekte ein wenig zu erweitern. Eine Kleisli-Kategorie für Maybe würde aus (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) Und id :: a -> Maybe a Bestehen. Das heißt, die Objekte in der Kategorie ergänzen den (->) Mit einem Maybe, sodass (a -> b) Zu (a -> Maybe b) Wird.

Und plötzlich haben wir die Kompositionsmöglichkeiten auf Dinge erweitert, bei denen die traditionelle Operation (.) Nicht funktioniert. Dies ist eine Quelle neuer Abstraktionskraft. Kleisli-Kategorien funktionieren mit mehr Typen als nur Maybe. Sie arbeiten mit jedem Typ, der eine richtige Kategorie zusammenstellen kann, unter Beachtung der Kategoriegesetze.

  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 einhält, können Sie ihn in eine Kleisli-Kategorie umwandeln. Und was ist das große Problem dabei? Nun, es stellt sich heraus, dass Monaden genau dasselbe sind wie die Kleisli-Kategorien. Monad 's return ist das gleiche wie Kleisli id. Monad 's (>>=) ist nicht identisch mit Kleisli (.), aber es stellt sich heraus, dass es sehr einfach ist, sich gegenseitig zu beschreiben. Und die Kategoriengesetze sind die gleichen wie die Monadengesetze, wenn Sie sie auf den Unterschied zwischen (>>=) Und (.) Übertragen.

Warum also diese ganze Mühe durchmachen? Warum hat die Sprache eine Monad Abstraktion? 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 geeignet ist. Es gibt das gesamte Paket monad-loops , das aus Schleifen besteht, die mit jeder Instanz von Monad funktionieren.

Die zweite Dimension ist indirekt, aber sie folgt aus der Existenz von Komposition. Wenn die Komposition einfach ist, ist es natürlich, Code in kleinen, wiederverwendbaren Stücken zu schreiben. Auf die gleiche Weise können mit dem Operator (.) Für Funktionen kleine wiederverwendbare Funktionen geschrieben werden.

Warum existiert die Abstraktion? Weil es sich als Werkzeug erwiesen hat, das mehr Code-Komposition 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 Gründe für die Programmierung. Die Monadenabstraktion existiert, weil sie uns ein wenig in Richtung dieses heiligen Grals bewegt.

212
Carl

Benjamin Pierce sagte in TAPL

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

Aus diesem Grund ist eine Sprache, die mit einem leistungsfähigen Typensystem ausgestattet ist, ausdrucksvoller als eine schlecht geschriebene Sprache. Sie können Monaden auf die gleiche Weise betrachten.

Mit @Carl und sigfpe point können Sie einen Datentyp mit allen gewünschten Operationen ausstatten, ohne auf Monaden, Typenklassen oder andere abstrakte Elemente zurückgreifen zu müssen. Mit Monaden können Sie jedoch nicht nur wiederverwendbaren Code schreiben, sondern auch alle überflüssigen Details abstrahieren.

Nehmen wir als Beispiel an, wir möchten eine Liste filtern. Am einfachsten ist es, die Funktion filter zu verwenden: filter (> 3) [1..10], die gleich [4,5,6,7,8,9,10] Ist.

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 alle i zu erhalten, so dass i <= 10, sum [1..i] > 4, sum [1..i] < 25, Können wir schreiben

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

das entspricht [3,4,5,6].

Oder wir können die Funktion nub neu definieren, mit der doppelte Elemente aus einer Liste entfernt werden, und zwar in Form von filterAccum:

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

nub' [1,2,4,5,4,3,1,8,9,4] Entspricht [1,2,4,5,3,8,9]. Hier wird eine Liste als Akkumulator übergeben. Der Code funktioniert, weil es möglich ist, die Liste als Monade zu belassen, sodass die gesamte Berechnung rein bleibt (notElem verwendet eigentlich nicht >>=, Könnte es aber). Es ist jedoch nicht möglich, die IO - Monade sicher zu verlassen (dh Sie können keine IO - Aktion ausführen und einen reinen Wert zurückgeben - der Wert wird immer in die eingeschlossen IO monad). Ein weiteres Beispiel sind veränderbare Arrays: Nachdem Sie die ST-Monade verlassen haben, in der sich ein veränderbares Array befindet, können Sie das Array nicht mehr in konstanter Zeit aktualisieren das Modul Control.Monad:

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)

filterM führt eine monadische Aktion für alle Elemente aus einer Liste aus und liefert Elemente, für die die monadische Aktion True zurückgibt.

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 monad, die fragt, 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 letztes Beispiel kann filterAccum definiert werden als filterM:

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

die unter der Haube verwendete StateT -Monade 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 Kompositionsfähigkeit von Monaden, wie @Carl erklärt), sondern auch benutzerdefinierte Datentypen und integrierte Grundelemente einheitlich behandeln können.

24
user3237465

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

Naiv ein IO System für Haskell bauen

Das einfachste denkbare IO 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 interaktive Terminalprogramme zu erstellen - sehr jedoch 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" Schreiben in eine Datei. Aber dann möchten Sie auch eine Möglichkeit, aus Dateien zu lesen. Irgendeine Chance?

Nun, wenn wir unser Programm main₁ Nehmen und einfach eine Datei an den Prozess leiten (unter Verwendung von Betriebssystemeinrichtungen), 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" String->[Output] Verwenden, ihm einen aus einer Datei erhaltenen String zuführen und ein nicht interaktives Programm ergeben, das einfach das gegebene ausführt.

Hier gibt es ein Problem: Wir haben nicht wirklich eine Vorstellung davon, ob wann die Datei gelesen wird. Die [Output] - Liste gibt den Ausgaben sicher eine schöne Reihenfolge, aber wir bekommen keine Reihenfolge, wann die Eingaben gemacht werden.

Lösung: Machen Sie Input-Events auch zu Elementen in der Liste der zu erledigenden Aufgaben.

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 vielleicht 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. Lesen Sie auch eine andere Datei. Offensichtliche Lösung: Machen Sie das Ergebnis der Eingabeereignisse auch zu etwas vom Typ IO, nicht nur zu Output. Das schließt sicher eine einfache Textausgabe ein, erlaubt 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 Dateioperation auszudrücken, die Sie sich in einem Programm wünschen (wenn auch 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 wirklich zuverlässigen Ü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 verwerfen und einfach ein "und dann machen" für jede Ausgabeoperation.

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 würden Sie keine einfachen Konstruktoren verwenden wollen, um alle Ihre Programme zu definieren. Es würde ein paar solcher grundlegender Konstruktoren geben müssen, aber für die meisten übergeordneten Dinge möchten wir eine Funktion mit einer netten übergeordneten Signatur schreiben. Es stellt sich heraus, dass die meisten davon ziemlich ähnlich aussehen würden: Akzeptieren Sie einen aussagekräftigen Wert und geben Sie als Ergebnis eine IO -Aktion aus.

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

Es gibt hier 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 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 (sonst) tatsächlich weiterzugeben Der Kontrollfluss des gesamten Programms kann leicht durch eine unlautere Aktion in der Mitte unterbrochen werden. Wir sollten diese Anforderung klarstellen. Nun, es stellt sich heraus, dass dies die Monadengesetze sind, obwohl ich nicht sicher bin, ob wir sie wirklich ohne die standardmäßigen 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

Offensichtlich ist dies keine effiziente Implementierung von IO, aber es ist im Prinzip verwendbar.

19
leftaroundabout

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

Nun ist die Art und Weise, wie sie zusammengesetzt werden, bei den vorhandenen Monaden unterschiedlich, was zu unterschiedlichen Verhaltensweisen führt (z. B. um einen veränderlichen Zustand in der Zustandsmonade zu simulieren).

Die Verwirrung über Monaden ist, dass sie so allgemein sind, dh ein Mechanismus zum Komponieren von Funktionen, dass sie für viele Dinge verwendet werden können, was die Leute glauben lässt, dass es bei Monaden um Staat geht, um E/A usw., wenn es nur darum geht, Funktionen zu komponieren ".

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 sehr praktisch, um beispielsweise eine klare Trennung zwischen reinem und unreinem Code zu implementieren: Deklarieren Sie alle unreinen Aktionen als Funktionen vom Typ "IO a" und stellen Sie keine Funktion bereit, wenn Sie die IO -Monade definieren , um den "a" -Wert aus dem "IO a" herauszunehmen. 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 zu nehmen, während sie rein bleibt (die Funktion muss innerhalb der "IO" -Monade sein, um verwendet zu werden solcher Wert). (HINWEIS: Naja, nichts ist perfekt, also kann die "IO Zwangsjacke" mit "unsafePerformIO: IO a -> a" zerbrochen werden, was eine reine Funktion sein sollte, aber dies sollte verwendet werden Sehr sparsam und wenn Sie wirklich wissen, dass Sie keinen unreinen Code mit Nebenwirkungen einführen.

3
mljrg

Monaden sind nur ein praktischer Rahmen zum Lösen einer Klasse von wiederkehrenden Problemen. Erstens müssen Monaden Funktoren sein (dh Mapping unterstützen, ohne auf die Elemente (oder deren Typ) zu achten), sie müssen auch eine Bindung (oder Verkettung) -Operation und a bringen Möglichkeit, einen monadischen Wert aus einem Elementtyp (return) zu erstellen. Schließlich müssen bind und return zwei Gleichungen (linke und rechte Identität) erfüllen, die auch als Monadengesetze bezeichnet werden. (Alternativ könnte man Monaden so definieren, dass sie einen flattening operation Anstelle einer Bindung haben.)

Das Listenmonade wird häufig 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) eine verschachtelte 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 repl Sitzung:

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

Es ist anzumerken, dass die Listenmonade in keiner Weise eine Nebenwirkungsberechnung ist. Eine mathematische Struktur, die eine Monade ist (d. H. Den oben genannten Grenzflächen und Gesetzen entspricht), impliziert keine Nebenwirkungen, obwohl Nebenwirkungsphänomene häufig gut in das monadische Gerüst passen.

3
heisenbug

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

Lassen Sie mich näher darauf eingehen. Sie haben Int, String und Real und Funktionen vom Typ Int -> String, String -> Real Und so weiter. Sie können diese Funktionen einfach kombinieren und mit Int -> Real Enden. Das leben ist gut.

Dann müssen Sie eines Tages eine neue Familie von Typen erstellen. 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 Maybe ein Typkonstruktor ist. Es akzeptiert einen Typ wie Int und gibt einen neuen Typ zurück Maybe Int. Erinnern Sie sich als erstes an kein Typkonstruktor, keine Monade.

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

Und hier kommen Monaden zur Rettung. Sie ermöglichen es Ihnen, diese Art von Funktionen erneut zu kombinieren. Sie müssen nur die Zusammensetzung . für > == ändern.

2
jdinunzio