2016-08-11 34 views
8

Próbowałem owinąć głowę wokół koncepcji monad i byłem eksperymentuje z poniższym przykładzie:i IO Monady

Mam typu danych Editor reprezentującą stan tekstu dokument i niektóre funkcje, które na nim działają.

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 

Wszystkie te funkcje działają na Editor, a wielu z nich powróci nowy Editor (gdzie daszek został przeniesiony lub jakiś tekst został zmieniony), więc pomyślałem, że może to być dobry stosowanie State monada i ja ponownie napisany najbardziej Editor działanie funkcji do teraz wyglądać następująco:

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 }) 

to dość niesamowite, ponieważ pozwala mi komponować czynności edycyjnych bardzo łatwo w ciągu do -notation.

Jednak obecnie staram się to wykorzystać w ramach rzeczywistej aplikacji. Powiedzmy, że chcę użyć tego Editor w aplikacji, która wykonuje niektóre IO. Załóżmy, że chcę manipulować instancją Editor za każdym razem, gdy użytkownik naciśnie klawisz l na klawiaturze.

musiałbym mieć inny State monady reprezentujący ogólny stan aplikacji, która posiada instancję Editor i sortowania zdarzenia pętli, która używa IO monady odczytu z klawiaturą i wywołuje moveHorizontally' zmodyfikować obecną AppState modyfikując jego Editor.

Przeczytałem trochę na ten temat i wydaje mi się, że muszę użyć Monad Transformers, aby zbudować stos monad z IO na dole. Nigdy wcześniej nie używałem Monad Transformers i nie wiem, co robić dalej? Dowiedziałem się też, że monada State implementuje już pewną funkcjonalność (wydaje się, że jest to szczególny przypadek Monad Transformer?), Ale jestem zdezorientowany, jak tego użyć?

+0

„Wszystkie te funkcje działają na edytorze, a wielu z nich zwraca nowy edytor (gdzie przeniesiono karetkę lub jakiś tekst został zmieniony) "- To dobrze! Jest bardzo mało prawdopodobne, aby zwykła pętla IO wykorzystująca twoje oryginalne czyste funkcje spowodowała utworzenie nowego edytora w pamięci.GHC skopiuje strukturę tylko wtedy, gdy zajdzie taka potrzeba, i zaktualizuje ją, jeśli stare odwołanie nie jest używane. Nie musisz tutaj używać transformatorów, a twój kod będzie bez nich wyraźniejszy. – thumphries

+0

@ DeX3 FYI, przesłanie * samowystarczalnego * posta ułatwia ludziom pisanie kodu, aby odpowiedzieć na twoje pytanie. – gallais

Odpowiedz

5

Najpierw cofnijmy się trochę. Zawsze najlepiej jest mieć problemy izolowane. Niech czyste funkcje będą zgrupowane z czystymi funkcjami, State - with State i IO - z IO. Intertwining wielu koncepcji to pewna recepta na gotowanie code-spaghetti. Nie chcesz tego posiłku.

Powiedziawszy to, przywróćmy czyste funkcje, które mieliśmy i pogrupuj je w moduł. Jednak będziemy stosować niewielkich modyfikacji, aby były zgodne z konwencjami Haskell - mianowicie, będziemy zmieniać kolejność parametrów:

-- | 
-- 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 

Teraz, jeśli naprawdę chcesz, aby uzyskać State API z powrotem, to trywialny do wdrożenia w innym module:

-- | 
-- 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) 

jak widać teraz, po standardowe konwencje pozwala nam na skorzystanie ze standardowych narzędzi State jak gets i modify do trywialnie podnieść już zaimplementowane funkcje do State monady.

Jednak wspomniane narzędzia pracują również dla monadransformatora StateT, z którego State jest w rzeczywistości tylko specjalnym przypadkiem. Więc równie dobrze może realizować to samo w sposób bardziej ogólny:

-- | 
-- 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) 

Jak widać, wszystko się zmieniło są podpisy typu.

Teraz możesz używać tych ogólnych funkcji w stosie transformatora. Np

-- | 
-- 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 

Zatem mamy właśnie osiągnął ziarnistą izolację obaw i dużą elastyczność naszym kodzie.

To oczywiście dość prymitywny przykład. Zazwyczaj ostatni stos monad-transformatora ma więcej poziomów. Np

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

Aby przejść między wszystkimi tymi poziomami typowy zestaw narzędzi jest the lift function lub the "mtl" library, który przewiduje typu zajęcia, aby zmniejszyć użycie lift. Muszę jednak wspomnieć, że nie wszyscy (łącznie z mną) są fanami "mtl", ponieważ, redukując ilość kodu, wprowadzają pewną niejednoznaczność i złożoność rozumowania. Wolę jawnie używać lift.

Punktem transformatorów jest umożliwienie rozszerzenia istniejącej monady (stos transformatora również jest monadą) z nową funkcjonalnością w sposób ad-hoc.

chodzi o twoje pytanie o Rozszerzanie aplikacji stan, wystarczy dodać kolejną warstwę StateT do stosu:

-- | 
-- 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

W porządku, to bardzo pomogło. Zrozumiałem kroki 1 i 2 i odpowiednio wyczyściłem mój kod. Jednak wciąż jestem trochę zdezorientowany tym, jak to wszystko się razem kończy. Na początek, czym dokładnie jest sygnatura typu 'moveHorizontally :: Monad m => Int -> StateT A.Editor m()' przekazać? 'moveHorizontally' to funkcja pobierająca' Int' i zwracająca StateTransformer gdzie stan jest 'A.Editor', a bezpośrednia wartość zwracana to jakaś' Monad' trzymająca pusty typ (ponieważ 'moveHorizontally' po prostu modyfikuje stan ale nie ma żadnego natychmiastowego wyniku)? Czy to prawda? – DeX3

+0

Mylisz 'StateT a m b' z' State a (m b) '. Są to różne rzeczy. 'Stan a b' jest równoważny funkcji' a -> (b, a) '. A zatem, 'Stan a (m b)' jest równoważny tej funkcji: 'a -> (m b, a)'. Natomiast "StateT a b b" jest równoważne "a -> m (b, a)". [Dokumentacja Hackage] (http://hackage.haskell.org/package/transformers-0.5.2.0/docs/Control-Monad-Trans-State-Strict.html#t:StateT) powinna pomóc ci trochę wyjaśnić . Mam również [artykuł szkolący intuicję w 'State'] (https://nikita-volkov.github.io/a-taste-of-state-parsers-are-easy/) przy użyciu parserów. –

+0

Tak, masz rację - pomyliłem 'State' z' StateT' tam. Dziękuję za wyjaśnienie. – DeX3

0

Użycie nie będzie wymagało zatwierdzenia do żadnego stosu monad, w szczególności do momentu, w którym program faktycznie uruchomi efekty. Oznacza to, że możesz łatwo zmienić stos, aby dodać dodatkowe warstwy, wybrać inną strategię raportowania błędów itd., Itd.

Wszystko, co musisz zrobić, to włączyć rozszerzenie językowe -XFlexibleContexts, dodając następujący wiersz na górze z pliku:

{-# LANGUAGE FlexibleContexts #-} 

importu moduł definiowania klasy MonadState:

import Control.Monad.State 

Zmień adnotacji typu programów w celu uwzględnienia faktu, że jesteś teraz przy użyciu tej metody. Ograniczenia MonadState Editor m => mówią, że m jest monadą, która gdzieś znajduje się w stanie typu Editor.

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

Powiedzmy, że teraz chcą czytać wiersz z stdin i odkłada ją na liście linii (w praktyce, że prawdopodobnie chcesz wstawić znaki po bieżącym CARRET i przenieść go odpowiednio ale ogólny pomysł jest taki sam).można po prostu użyć MonadIO presję, aby wskazać, że trzeba pewne możliwości IO dla tej funkcji:

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