2016-08-11 2 views
8

Ich habe versucht, meinen Kopf um das Konzept der Monaden zu wickeln, und ich habe mit dem folgenden Beispiel experimentiert:Staat und IO Monaden

Ich habe eine Editor Datentyp, der den Status eines Textes darstellt Dokument und einige Funktionen, die daran arbeiten.

data Editor = Editor { 
    lines :: [Line], -- editor contents are kept line by line  
    lineCount :: Int, -- holds length lines at all times 
    caret :: Caret  -- the current caret position 
    -- ... some more definitions 
} deriving (Show) 

-- get the line at the given position (first line is at 0) 
lineAt :: Editor -> Int -> Line 
lineAt ed n = ls !! n 
    where 
    ls = lines ed 

-- get the line that the caret is currently on 
currentLine :: Editor -> Line 
currentLine ed = lineAt ed $ currentY ed 

-- move the caret horizontally by the specified amount of characters (can not 
-- go beyond the current line) 
moveHorizontally :: Editor -> Int -> Editor 
moveHorizontally ed n = ed { caret = newPos } 
    where 
    Caret x y = caret ed 
    l = currentLine ed 
    mx = fromIntegral (L.length l - 1) 
    newX = clamp 0 mx (x+n) 
    newPos = Caret newX y 


-- ... and lots more functions to work with an Editor 

Alle diese Funktionen wirken sich auf einem Editor, und viele von ihnen zurückkehren ein neues Editor (wo der Cursor oder einen Text wurde geändert, verschoben wurde), so dachte ich, das ist eine gute Anwendung des State sein könnte Monade und ich habe neu geschrieben meisten Editor -Funktionen nun wie folgt aussehen:

lineAt' :: Int -> State Editor Line 
lineAt' n = state $ \ed -> (lines ed !! n, ed) 

currentLine' :: State Editor Line 
currentLine' = do 
    y <- currentY' 
    lineAt' y 

moveHorizontally' :: Int -> State Editor() 
moveHorizontally' n = do 
    (Caret x y) <- gets caret 
    l <- currentLine' 
    let mx = fromIntegral (L.length l - 1) 
    let newX = clamp 0 mx (x+n) 
    modify (\ed -> ed { caret = Caret newX y }) 

moveHorizontally' :: Int -> State Editor() 
moveHorizontally' n = do 
    (Caret x y) <- gets caret 
    l <- currentLine' 
    let mx = fromIntegral (L.length l - 1) 
    let newX = clamp 0 mx (x+n) 
    modify (\ed -> ed { caret = Caret newX y }) 

Das ist ziemlich genial, weil es mir Bearbeitungsaktionen innerhalb do -Notation sehr leicht komponieren können.

Wie auch immer, jetzt habe ich Mühe, dies in einer tatsächlichen Anwendung zu verwenden. Angenommen, ich möchte dieses Editor innerhalb einer Anwendung verwenden, die einige IO ausführt. Angenommen, ich möchte eine Instanz von Editor jedes Mal manipulieren, wenn der Benutzer die Taste l auf der Tastatur drückt.

I würde eine andere State monadisch haben müssen, den gesamten Anwendungszustand darstellt, der eine Editor Instanz und einen Art-of Event-Loop, die den IO monadisch von der Tastatur zum Lesen verwendet hält und rufen moveHorizontally' die aktuellen AppState zu modifizieren durch Modifizieren sein Editor.

Ich habe ein wenig zu diesem Thema gelesen und es scheint, als würde ich Monad Transformers verwenden, um einen Stapel von Monaden mit IO an der Unterseite zu bauen. Ich habe noch nie Monad Transformers benutzt und weiß nicht, was ich von hier machen soll? Ich habe auch herausgefunden, dass die State Monade bereits einige Funktionen implementiert (es scheint ein Spezialfall eines Monade Transformers zu sein?), Aber ich bin verwirrt, wie man das verwendet?

+0

„All diese Funktionen wirken sich auf einem Editor, und viele von ihnen einen neuen Editor zurückzukehren (wo der Caret verschoben wurde oder ein Text geändert wurde "- Das ist eine gute Sache! Es ist sehr unwahrscheinlich, dass eine einfache IO-Schleife, die Ihre ursprünglichen reinen Funktionen anwendet, einen neuen Editor im Speicher erzeugen würde.GHC kopiert die Struktur nur dann, wenn es erforderlich ist, und aktualisiert sie direkt, wenn die alte Referenz nicht verwendet wird. Sie müssen hier keine Transformatoren verwenden, und ohne sie wird Ihr Code klarer. – thumphries

+0

@ DeX3 FYI, Einreichen eines * in sich geschlossenen * Post macht es viel einfacher für Menschen, Code zu schreiben, um Ihre Frage zu beantworten. – gallais

Antwort

5

Lassen Sie uns zunächst ein wenig sichern. Es ist immer am besten, Probleme isoliert zu haben. Lassen Sie reine Funktionen mit reinen Funktionen gruppieren, State - mit State und IO - mit IO. Das Ineinandergreifen mehrerer Konzepte ist ein bestimmtes Rezept zum Kochen von Code-Spaghetti. Du willst dieses Essen nicht.

Nachdem wir das gesagt haben, stellen wir die reinen Funktionen wieder her und gruppieren sie in einem Modul. Allerdings werden wir kleine Änderungen anwenden, um sie an die Haskell Konventionen zu machen entsprechen - nämlich, werden wir die Parameter ändern, um:

-- | 
-- In this module we provide all the essential functions for 
-- manipulation of the Editor type. 
module MyLib.Editor where 

data Editor = ... 

lineAt :: Int -> Editor -> Line 

moveHorizontally :: Int -> Editor -> Editor 

Nun, wenn Sie wirklich Ihre State API zurück erhalten möchten, ist es trivial zu implementieren in einem anderen Modul:

-- | 
-- In this module we address the State monad. 
module MyLib.State where 

import qualified MyLib.Editor as A 

lineAt :: Int -> State A.Editor Line 
lineAt at = gets (A.lineAt at) 

moveHorizontally :: Int -> State A.Editor() 
moveHorizontally by = modify (A.moveHorizontally by) 

wie Sie jetzt sehen, nach den Standardkonventionen ermöglicht es uns, die Standard-State Utilities wie gets und modify zu trivialer Weise heben die bereits implementierten Funktionen zum State Monade zu verwenden.

Allerdings funktionieren die genannten Dienstprogramme auch für den StateT Monade-Transformator, von denen State eigentlich nur ein Sonderfall ist. So können wir nur umsetzen als auch die gleiche Sache in einer allgemeineren Weise:

-- | 
-- In this module we address the StateT monad-transformer. 
module MyLib.StateT where 

import qualified MyLib.Editor as A 

lineAt :: Monad m => Int -> StateT A.Editor m Line 
lineAt at = gets (A.lineAt at) 

moveHorizontally :: Monad m => Int -> StateT A.Editor m() 
moveHorizontally by = modify (A.moveHorizontally by) 

Wie Sie sehen, alles, was sich geändert hat sind die Typsignaturen.

Jetzt können Sie diese allgemeinen Funktionen in Ihrem Transformatorstapel verwenden. ZB

-- | 
-- In this module we address the problems of the transformer stack. 
module MyLib.Session where 

import qualified MyLib.Editor as A 
import qualified MyLib.StateT as B 

-- | Your trasformer stack 
type Session = StateT A.Editor IO 

runSession :: Session a -> A.Editor -> IO (a, A.Editor) 
runSession = runStateT 

lineAt :: Int -> Session Line 
lineAt = B.lineAt 

moveHorizontally :: Int -> Session() 
moveHorizontally = B.moveHorizontally 

-- | 
-- A function to lift the IO computation into our stack. 
-- Luckily for us it is already presented by the MonadIO type-class. 
-- liftIO :: IO a -> Session a 

So haben wir gerade eine granulare Isolierung von Bedenken und eine große Flexibilität unserer Codebasis erreicht.

Nun, natürlich macht dies ein ziemlich primitives Beispiel bis jetzt. Normalerweise hat der letzte Monade-Transformator-Stapel mehr Pegel. Z. B.

type Session = ExceptT Text (ReaderT Database (StateT A.Editor IO)) 

Um das typische Tool-Set the lift function oder the "mtl" library ist, die Typ-Klassen bietet die Verwendung von lift zu reduzieren, die zwischen all diesen Ebenen zu springen. Ich muss jedoch erwähnen, dass nicht jeder (auch ich selbst) ein Fan von "mtl" ist, da es zwar die Menge an Code reduziert, aber eine gewisse Mehrdeutigkeit und logische Komplexität mit sich bringt. Ich bevorzuge es, lift explizit zu verwenden.

Der Punkt von Transformatoren ist es, Ihnen zu ermöglichen, eine existierende Monade (Transformer Stack ist eine Monade) mit einigen neuen Funktionen ad hoc zu erweitern.

Was Ihre Frage über den Zustand der App erstreckt, können Sie einfach eine andere StateT Schicht auf Ihren Stack hinzufügen:

-- | 
-- In this module we address the problems of the transformer stack. 
module MyLib.Session where 

import qualified MyLib.Editor as A 
-- In presence of competing modules, 
-- it's best to rename StateT to the more specific EditorStateT 
import qualified MyLib.EditorStateT as B 
import qualified MyLib.CounterStateT as C 

-- | Your trasformer stack 
type Session = StateT Int (StateT A.Editor IO) 

lineAt :: Int -> Session Line 
lineAt = lift B.lineAt 

moveHorizontally :: Int -> Session() 
moveHorizontally = lift B.moveHorizontally 

-- | An example of addressing a different level of the stack. 
incCounter :: Session() 
incCounter = C.inc 

-- | An example of how you can dive deeply into your stack. 
liftIO :: IO a -> Session a 
liftIO io = lift (lift io) 
+0

In Ordnung, das hat sehr geholfen. Ich habe die Schritte 1 und 2 verstanden und meinen Code entsprechend aufgeräumt. Allerdings bin ich noch ein wenig verwirrt darüber, wie alles am Ende zusammenkommt. Als Ausgangspunkt, was genau wird die Typ-Signatur 'moveHorizontally :: Monad m => Int -> StateT A.Editor m()' übermitteln? 'moveHorizontally' ist eine Funktion, die einen' Int' nimmt und einen StateTransformer zurückgibt, wobei der Zustand ein 'A.Editor' ist und der unmittelbare Rückgabewert ein' Monad' ist, der den leeren Typ hält (weil 'moveHorizontally' nur den Zustand ändert) aber hat kein sofortiges Ergebnis)? Ist das richtig? – DeX3

+0

Sie verwechseln 'StateT a m b' mit' State a (m b) '. Sie sind verschiedene Dinge. Der Zustand a b entspricht der Funktion a -> (b, a). Also ist "State a (m b)" äquivalent zu dieser Funktion: 'a -> (m b, a)'. "StateT a m b" ist andererseits äquivalent zu "a -> m (b, a)". [Die Hackage-Dokumentation] (http://hackage.haskell.org/package/transformers-0.5.2.0/docs/Control-Monad-Trans-State-Strict.html#t:StateT) sollte Ihnen helfen, die Dinge ein wenig zu klären . Außerdem habe ich [einen Artikel, der die Intuition in "State" trainiert (https://nikita-volkov.github.io/a-taste-of-state-parsers-are-easy/) Parser verwenden. –

+0

yep, du hast Recht - ich habe "State" mit "StateT" verwechselt. Danke für Ihre Erklärung. – DeX3

0

Mit mtl müssen Sie sich vor dem Programmstart, an dem Sie die Effekte tatsächlich ausführen, nicht an einen Monad-Stack binden. Dies bedeutet, dass Sie den Stapel problemlos ändern können, um zusätzliche Ebenen hinzuzufügen, eine andere Fehlerberichts-Strategie usw. auszuwählen.

Sie müssen lediglich die Sprachenerweiterung -XFlexibleContexts aktivieren, indem Sie oben die folgende Zeile hinzufügen Ihrer Datei:

{-# LANGUAGE FlexibleContexts #-} 

Import das Modul die MonadState Klasse definieren:

import Control.Monad.State 

ändern der Typanmerkung Ihrer Programme um die Tatsache widerzuspiegeln, dass Sie nun diesen Ansatz verwenden. Die MonadState Editor m => Constraints sagen, dass m eine Monade ist, die einen Zustand des Typs Editor irgendwo darin hat.

lineAt'  :: MonadState Editor m => Int -> m Line 
currentY' :: MonadState Editor m => m Int 
currentLine' :: MonadState Editor m => m Line 

Lassen Sie uns sagen, dass Sie jetzt eine Linie von stdin und schieben Sie es auf die Liste der Linien (in der Praxis lesen möchten würden Sie wahrscheinlich die Zeichen nach der aktuellen carret eingefügt werden soll und entsprechend bewegen, aber die allgemeine Idee ist das gleiche).Sie können einfach die MonadIO Einschränkung verwenden, um anzuzeigen, dass Sie einige IO Fähigkeit für diese Funktion benötigen:

newLine :: (MonadIO m, MonadState Editor m) => m() 
newLine = do 
    nl <- liftIO getLine 
    modify $ \ ed -> ed { lines = nl : lines ed }