2016-08-11 3 views
8

내가 모나드의 개념 주위에 내 머리를 정리하려고했는데 나는 다음과 같은 예를 실험했습니다 :주 및 IO 모나드

나는 텍스트의 상태를 나타내는 Editor 데이터 유형이 문서 및 그것에 작동하는 몇 가지 기능을 제공합니다.

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 

이러한 모든 기능

Editor에 따라 행동하고, 그들 중 많은 사람들이 돌아 새로운 Editor (caret의 이동이나 된 경우 일부 텍스트가 변경되었습니다) 그래서 나는 이것이 State의 좋은 응용 프로그램이 될 줄 알았는데 모나드와 나는 대부분의 지금과 같이 할 수 Editor -functions를 다시 작성했습니다 :

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

그것은 나를 do 표기법에서 아주 쉽게 편집 작업을 구성 할 수 있기 때문에이 꽤 굉장합니다.

그러나 지금은 실제 응용 프로그램 내에서 사용하도록 고심하고 있습니다. IO를 수행하는 응용 프로그램에서이 코드를 Editor으로 사용한다고 가정 해보십시오. 사용자가 키보드의 l 키를 누를 때마다 Editor의 인스턴스를 조작하려고한다고 가정 해보십시오.

내가 Editor 인스턴스 수정하여 현재의 AppState을 수정 moveHorizontally'을 키보드에서 읽을 IO 모나드를 사용하고 호출하는 정렬의 이벤트 루프를 보유하고있는 전체 애플리케이션 상태를 나타내는 또 다른 State 모나드를해야합니다 그것의 Editor.

나는이 주제에 조금 읽어 봤는데, 하단에 IO와 모나드의 스택을 구축 할 모나드 트랜스 포머를 사용해야하는 것처럼 보인다. Monad Transformers는 한번도 사용 해본 적이 없으며 여기서 무엇을해야할지 모르겠습니다. 나는 또한 State 모나드가 이미 일부 기능을 구현한다는 것을 알았지 만 (모나드 변압기의 특수한 경우 인 것 같습니까?) 사용 방법에 대해 혼란 스럽습니다.

+0

는 "이러한 모든 기능은 에디터에 따라 행동하고, 그들 중 많은 (새로운 편집기를 반환 캐럿이 이동되었거나 일부 텍스트가 변경된 곳) "- 이것은 좋은 것입니다! 원래의 순수 함수를 적용한 일반 IO 루프가 메모리에 새 편집기를 생성하는 것은 거의 없습니다.GHC는 필요한 경우에만 구조체를 복사하고 이전 참조가 사용되지 않으면 내부에서 업데이트합니다. 여기서 변압기를 반드시 사용할 필요는 없으며 코드가 없으면 코드가 더 명확 해집니다. – thumphries

+0

@ DeX3 참고로 * 자체 포함 * 게시물을 제출하면 사람들이 귀하의 질문에 답변하기 위해 코드를 작성하는 것이 훨씬 쉬워집니다. – gallais

답변

5

먼저, 조금을 백업 할 수 있습니다. 항상 문제를 격리시키는 것이 가장 좋습니다. 순수 함수가 순수 함수로 그룹화되도록하십시오. State - State와 IO - IO가 있습니다. 여러 개념을 얽어 짜는 것은 코드 스파게티를 요리하는 특정 조리법입니다. 너는 그 식사를 원하지 않는다.

그렇다면, 가지고 있던 순수한 함수를 복원하고 모듈로 그룹화 해보자. - 그러나 우리는 그들에게 하스켈 규칙을 준수하기 위해 작은 수정을 적용 할 수 있습니다 즉, 우리는 매개 변수 순서를 변경할 수 있습니다 : 당신이 정말로 다시 State API를 받기를 원한다면, 지금

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

을, 그것을 구현하는 사소한 다른 모듈에서 : 당신은 지금보다시피

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

는 표준 규칙을 다음과 같은 것은 우리가 하찮게 State 모나드에 이미 구현 기능을 들어 올려 getsmodify 같은 표준 State 유틸리티를 사용할 수 있습니다.

실제로 언급 된 유틸리티는 모나드 트랜스포머에서도 작동하며 그 중 State은 실제로 특별한 경우입니다. 그래서 우리는 단지뿐만 아니라보다 일반적인 방법으로 같은 일을 구현할 수 있습니다

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

보시다시피, 모두는 유형 서명이 변경된.

이제 트랜스포머 스택에서 이러한 일반 기능을 사용할 수 있습니다. 예 :

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

따라서 우리는 막대한 관심과 코드베이스의 유연성을 제공합니다.

물론 지금까지 아주 원시적 인 예가되었습니다. 보통 최종 모나드 - 트랜스포머 스택에는 더 많은 레벨이 있습니다. 예를 들어,

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

lift의 사용을 줄이기 위해 타입 클래스를 제공하는 일반적인 도구 세트 the lift function 또는 the "mtl" library 모든 그 수준 사이를 이동합니다. 나는 모든 사람들이 (자신을 포함해서) "mtl"의 팬이 아니라는 것을 언급해야한다. 왜냐하면 코드의 양을 줄이면서 특정 모호성과 추론 복잡성을 가져 오기 때문이다. 나는 lift을 명시 적으로 사용하는 것을 선호한다.

변압기의 요점은 기존 기능 모나드 (변압기 스택도 모나드 임)를 임의의 새로운 기능으로 확장 할 수 있도록하는 것입니다.

응용 프로그램의 상태를 확장에 대한 질문에 관해서는, 당신은 단순히 스택에 다른 StateT 층을 추가 할 수 있습니다

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

좋아요, 그게 많은 도움이되었습니다. 나는 1, 2 단계를 이해하고 적절하게 코드를 정리했다. 그러나 나는 그것이 모두 결국 함께하는 방법에 의해 조금 아직도 혼란스러워. 출발점으로, 타입 시그니처'moveHorizontally :: Monad m => Int -> StateT A.Editor m()'은 정확히 무엇을 전달합니까? 'moveHorizontally'는'Int'를 취하여 State가 A.Editor 인 StateTransformer를 반환하는 함수이며, 즉각적인 반환 값은 빈 타입을 가진'Monad'입니다 (왜냐하면'moveHorizontally'는 상태를 수정하기 때문입니다). 즉각적인 결과가 없음)? 그게 맞습니까? – DeX3

+0

'StateT a m b'와'State a (m b)'를 혼동하고 있습니다. 그것들은 다른 것들입니다. 'State a b'는'a -> (b, a)'함수와 동일합니다. 그래서'State a (m b)'는이 함수와 같습니다 :'a -> (m b, a)'. 반면에'StateT a m b'는'a -> m (b, a)'와 같습니다. [The Hackage documentation] (http://hackage.haskell.org/package/transformers-0.5.2.0/docs/Control-Monad-Trans-State-Strict.html#t:StateT)는 약간의 정보를 정리하는 데 도움이됩니다. . 또한 저는 파서를 사용하여 [국가의 직관을 훈련하는 기사] (https://nikita-volkov.github.io/a-taste-of-state-parsers-are-easy/)를 가지고 있습니다. –

+0

예, 당신 말이 맞아요. "State"와 "StateT"를 혼동했다. 당신의 설명에 감사드립니다. – DeX3

0

mtl을 사용하면 실제로 효과를 실행하는 프로그램 시점까지 모나드 스택을 커밋 할 필요가 없습니다. 즉, 스택을 쉽게 변경하여 추가 레이어를 추가하거나 다른 오류보고 전략 등을 선택할 수 있습니다.

상단에 다음 줄을 추가하여 언어 확장 -XFlexibleContexts을 사용하도록 설정하면됩니다. 파일의 :

{-# LANGUAGE FlexibleContexts #-} 

는 가져 MonadState 클래스를 정의 모듈 :

import Control.Monad.State 

변경 프로그램의 유형 약어는 지금이 방법을 사용하고 있다는 사실을 반영합니다. MonadState Editor m => 제약 조건은 mEditor 상태 인 모나드라고합니다.

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

의 당신이 지금 stdin에서 라인을 읽고 당신은 아마 일반을 현재 carret 후 문자를 삽입하고 그에 따라 이동하지만 싶어 실제로 (라인의 목록에 밀어 싶어한다고 가정 해 봅시다 아이디어는 동일합니다.)당신은 단순히이 기능에 대한 몇 가지 IO 능력이 필요하다는 것을 나타 내기 위해 MonadIO 제약 조건을 사용할 수 있습니다

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