州和IO Monads

DeX*_*eX3 8 monads haskell state-monad monad-transformers

我一直在尝试围绕monad的概念,我一直在尝试以下示例:

我有一个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
Run Code Online (Sandbox Code Playgroud)

所有这些函数都作用于a Editor,并且其中许多函数返回一个新的Editor(插入符号已被移动或某些文本已被更改)所以我认为这可能是Statemonad的一个很好的应用程序,我重写了大多数Editor函数现在看起来像这样:

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 })
Run Code Online (Sandbox Code Playgroud)

这非常棒,因为它允许我在do注释中非常容易地编写编辑操作.

但是,现在我正在努力将其用于实际应用程序中.假设我想Editor在执行某些IO的应用程序中使用它.假设我想操纵Editor每次用户按下l键盘上的键的实例.

我需要另一个Statemonad代表整个应用程序状态,它包含一个Editor实例和一个事件循环,它使用IOmonad从键盘读取并moveHorizontally'通过修改它来调用修改当前的AppState Editor.

我已经阅读了一些关于这个主题的内容,似乎我需要使用Monad Transformers来构建一个底部有IO的monad堆栈.我以前从未使用过Monad变形金刚,我不知道该怎么办?我还发现Statemonad已经实现了一些功能(它似乎是Monad Transformer的一个特例?)但我对如何使用它感到困惑?

Nik*_*kov 5

首先,让我们回顾一下.最好将问题隔离开来.让纯函数与纯函数,状态 - 状态和IO - 与IO组合在一起.交织多个概念是烹饪代码意大利面的一种方法.你不想吃那顿饭.

话虽如此,让我们恢复您拥有的纯函数并将它们分组到模块中.但是,我们将应用小修改使它们符合Haskell约定 - 即,我们将更改参数顺序:

-- |
-- 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
Run Code Online (Sandbox Code Playgroud)

现在,如果你真的想要恢复你的StateAPI,那么在另一个模块中实现它是微不足道的:

-- |
-- 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)
Run Code Online (Sandbox Code Playgroud)

正如您现在看到的那样,遵循标准约定允许我们使用标准State实用程序,例如gets并将modify已实现的函数简单地提升到Statemonad.

然而,实际上所提到的实用程序也适用于StateTmonad-transformer,其中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)
Run Code Online (Sandbox Code Playgroud)

如您所见,所有改变的都是类型签名.

现在,您可以在变换器堆栈中使用这些常规函数.例如,

-- |
-- 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
Run Code Online (Sandbox Code Playgroud)

因此,我们刚刚实现了关注点的细化隔离以及代码库的极大灵活性.

当然,现在,这是一个非常原始的例子.通常,最终的monad-transformer堆栈具有更多级别.例如,

type Session = ExceptT Text (ReaderT Database (StateT A.Editor IO))
Run Code Online (Sandbox Code Playgroud)

所有这些层次之间跳跃的典型工具集是lift功能还是在"MTL"库,它提供了型类,以减少使用lift.我不得不提一下,并非每个人(包括我自己)都是"mtl"的粉丝,因为在减少代码量的同时,它引入了一定的模糊性和推理复杂性.我更喜欢lift明确使用.

变换器的要点是允许您以特殊的方式扩展现有的monad(变换器堆栈也是monad)和一些新功能.

至于关于扩展应用程序状态的问题,您可以简单地向堆栈添加另一个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)
Run Code Online (Sandbox Code Playgroud)