2012-06-06 3 views
10

Ich versuche, einen Spider Solitaire Player als Haskell Lernübung zu schreiben.Kombinieren von Monaden in Haskell

Meine main Funktion ruft eine playGame Funktion einmal für jedes Spiel (mit mapM), vorbei an der Spielnummer und einen Zufallsgenerator (StdGen). Die playGame Funktion sollte eine Control.Monad.State Monade und eine IO-Monade zurückgeben, die eine String enthält, die das Spieltableau und eine Bool anzeigt, ob das Spiel gewonnen oder verloren wurde.

Wie kombiniere ich die State Monade mit der IO Monade für den Rückgabewert? Was sollte die Typdeklaration für `playGame sein?

playGame :: Int -> StdGen a -> State IO (String, Bool) 

Ist die State IO (String, Bool) korrekt? Wenn nicht, was sollte es sein?

In main, ich plane über die Verwendung

do 
    -- get the number of games from the command line (already written) 
    results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames] 

Ist dies der richtige Weg playGame zu nennen?

+0

Sie könnten auch 'RandT' aus dem [MonadRandom] (http://hackage.haskell.org/package/MonadRandom) Paket genießen. –

Antwort

12

Was Sie wollen, ist StateT s IO (String, Bool), wo StateT sowohl Control.Monad.State vorgesehen ist (aus dem mtl-Paket) und Control.Monad.Trans.State (aus dem transformers Paket).

Dieses allgemeine Phänomen wird als Monade-Transformator bezeichnet, und Sie können eine gute Einführung zu ihnen in Monad Transformers, Step by Step lesen.

Es gibt zwei Ansätze, sie zu definieren. Eine davon befindet sich im Paket transformers, das die Klasse MonadTrans verwendet, um sie zu implementieren. Der zweite Ansatz befindet sich in der Klasse mtl und verwendet eine separate Typklasse für jede Monade.

Der Vorteil des transformers Ansatz ist die Verwendung eines einzigen Typ-Klasse ist alles zu implementieren (gefunden here):

class MonadTrans t where 
    lift :: Monad m => m a -> t m a 

lift hat zwei nette Eigenschaften, die jede Instanz von MonadTrans erfüllen muss:

(lift .) return = return 
(lift .) f >=> (lift .) g = (lift .) (f >=> g) 

Dies sind die functor Gesetze in Verkleidung, wo (lift .) = fmap, return = id und (>=>) = (.).

Der mtl Typ-Class-Ansatz hat seine Vorteile, auch, und einige Dinge nur sauber sein können gelöst mit den mtl Typ-Klassen, aber der Nachteil ist dann, dass jede mtl Typ-Klasse ihre eigenen Gesetze hat man sich erinnern, wenn Instanzen für es implementieren. Zum Beispiel kann die MonadError Typ-Klasse (gefunden here) ist definiert als:

class Monad m => MonadError e m | m -> e where 
    throwError :: e -> m a 
    catchError :: m a -> (e -> m a) -> m a 

Diese Klasse von Gesetzen kommt, auch:

m `catchError` throwError = m 
(throwError e) `catchError` f = f e 
(m `catchError` f) `catchError` g = m `catchError` (\e -> f e `catchError` g) 

Dies sind nur die Monade Gesetze in der Verkleidung, wo throwError = return und catchError = (>>=) (und die Monad-Gesetze sind die Kategorie-Gesetze in Verkleidung, wo return = id und (>=>) = (.)).

Für Ihr spezielles Problem, wie Sie Ihr Programm schreiben würde wäre das gleiche:

do 
    -- get the number of games from the command line (already written) 
    results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames] 

... aber wenn Sie Ihre playGame Funktion schreiben, würde es aussehen, entweder wie:

-- transformers approach :: (Num s) => StateT s IO() 
do x <- get 
    y <- lift $ someIOAction 
    put $ x + y 

-- mtl approach :: (Num s, MonadState s m, MonadIO m) => m() 
do x <- get 
    y <- liftIO $ someIOAction 
    put $ x + y 

Es gibt mehr Unterschiede zwischen den Ansätzen, die deutlicher werden, wenn Sie mit dem Stapeln von mehr als einem Monade-Transformator beginnen, aber ich denke, das ist ein guter Anfang.

+0

Sehr schöne und vollständige Antwort. Vielen Dank. – Ralph

+1

'StateT IO (String, Bool)' ist falsch - eine Art Nichtübereinstimmung. Es ist 'StateT s m a' mit' s' der Zustandstyp und 'm' eine Monade und' a' der Ergebnistyp. –

+0

Auch der 'mtl'-Ansatz und der' transformers'-Ansatz sind eigentlich keine unterschiedlichen Wege, um das Gleiche zu tun - 'MonadError' erreicht ein anderes Ziel als' MonadTrans'. Und die Gesetze sind nicht die verkleideten "Monaden" -Gesetze - sie sind sehr ähnlich *, aber die Art der Dinge und ihre Bedeutung ist anders. Ich meine, sie sind im Grunde ein Einheitsgesetz und ein assoziatives Gesetz, aber in Bezug auf ganz andere Operationen. –

8

State ist eine Monade, und IO ist eine Monade. Was Sie von Grund auf schreiben wollen, wird als "Monade Transformer" bezeichnet, und die Haskell Standardbibliothek definiert bereits, was Sie brauchen.

Werfen Sie einen Blick auf die State-Monade-Transformator StateT: Es hat einen Parameter, der die innere Monade ist, die Sie in die State einpacken möchten.

Jeder Monade-Transformer implementiert eine Reihe von Typklassen, so dass der Transformer für jede Instanz jedes Mal damit umgeht (z. B. kann der Statustransformator nur zustandsabhängige Funktionen verarbeiten) oder den Aufruf propagiert auf die innere Monade in der Weise, dass, wenn Sie alle Transformatoren, die Sie wollen, stapeln und haben eine einheitliche Schnittstelle für den Zugriff auf die Funktionen von allen von ihnen. Es ist eine Art von chain of responsibility, wenn Sie es auf diese Weise betrachten möchten.

Wenn Sie auf hackage schauen, oder eine schnelle Suche auf Stack Overflow oder Google finden Sie viele Beispiele für die Verwendung von StateT finden.

bearbeiten: Eine weitere interessante Lektüre ist Monad Transformers Explained.

+1

Ich mag es, wie man das Rad mehrmals wiedererkennt und Haskell lernt ... Es ist eigentlich ziemlich schön, eine Lösung für ein Problem zu finden und herauszufinden, dass es ein allgemeines Designmuster ist. – fuz

+1

@FUZxxl: Ja, es ist in der Tat :) –

2

Okay, ein paar Dinge zu klären hier oben:

  • Sie können nicht "eine Monade zurückkehren". Eine Monade ist eine Art Typ, keine Art von Wert (um genau zu sein, ist eine Monade ein Typ Konstruktor, die eine Instanz der Monad Klasse hat). Ich weiß, das klingt pedantisch, aber es könnte Ihnen helfen, die Unterscheidung zwischen Dingen und Arten von Dingen in Ihrem Kopf zu ordnen, was wichtig ist.
  • Beachten Sie, dass Sie nichts mit State tun können, die ohne es unmöglich ist, also wenn Sie verwirrt sind, wie man es benutzt, dann fühlen Sie nicht, dass Sie müssen! Oft schreibe ich einfach den normalen Funktionstyp, den ich möchte, und dann, wenn ich merke, dass ich viele Funktionen wie Thing -> (Thing, a) habe, würde ich "aha, das sieht ein bisschen wie State aus, vielleicht kann dies vereinfacht werden State Thing a". Das Verstehen und Arbeiten mit einfachen Funktionen ist ein wichtiger erster Schritt auf dem Weg zur Verwendung von State oder seiner Freunde.
  • IO, auf der anderen Seite, ist das einzige, was seine Arbeit machen kann. Aber der Name playGame springt mir nicht sofort als der Name von etwas vor, das I/O machen muss.Insbesondere wenn Sie nur (Pseudo-) Zufallszahlen benötigen, können Sie dies ohne IO tun. Wie ein Kommentator darauf hingewiesen hat, MonadRandom ist großartig, um dies einfach zu machen, aber wieder können Sie nur reine Funktionen verwenden, die eine StdGen von System.Random nehmen und zurückgeben. Sie müssen nur sicherstellen, dass Sie Ihren Samen (StdGen) richtig fädeln (dies automatisch zu tun war im Grunde, warum State erfunden wurde; Sie könnten es besser verstehen, nachdem Sie ohne es zu programmieren versuchen)
  • Schließlich sind Sie nicht ganz richtig verwenden getStdGen. Es ist eine IO Aktion, also müssen Sie das Ergebnis mit <- in einem do-Block binden, bevor Sie es verwenden (technisch nicht brauchen Sie, Sie haben viele Optionen, aber das ist fast sicher, was Sie tun möchten) . Etwas wie dieses:

    do 
        seed <- getStdGen 
        results <- mapM (\game -> playGame game seed) [1..numberOfGames] 
    

    Hier playGame :: Integer -> StdGen -> IO (String, Bool). Beachten Sie jedoch, dass Sie die gleiche Random Seed an jede playGame übergeben, die möglicherweise nicht das ist, was Sie wollen. Wenn das nicht der Fall ist, könntest du den Samen von jedem playGame zurückgeben, wenn du damit fertig bist, um zum nächsten zu kommen, oder wiederholt neue Samen mit newStdGen (was du von innen tun könntest, wenn du dich entscheidest) behalte es in IO).

Wie dem auch sei, hat dies eine sehr strukturierte Antwort gewesen, für die ich entschuldige mich, aber ich hoffe, es gibt Ihnen etwas zu denken.

+0

"* Eine Monade ist eine Art * Typ, * keine Art von Wert. *" Wäre es nicht fair zu behaupten, dass es sich tatsächlich um einen Typkonstruktor handelt? – Ashe

+1

Ja, eine Monade ist eine Art Typkonstruktor, aber ich wollte nicht zu technisch klingen - ich wollte nur betonen, dass "Monaden zur Welt der Typen gehören". Ich werde es genauer bearbeiten. –