州和IO Monads [英] State and IO Monads

查看:67
本文介绍了州和IO Monads的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直试图围绕单子概念展开尝试,并且一直在尝试以下示例:

I've been trying to wrap my head around the concept of monads and I've been experimenting with the following example:

我有一个Editor数据类型,它表示文本文档的状态以及在其上起作用的一些功能.

I have an Editor data-type that represents the state of a text document and some functions that work on it.

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(插入符号已移动或某些文本已更改),所以我认为这可能是State monad和我重新编写了大多数Editor函数,现在看起来像这样:

All of these functions act on an Editor, and many of them return a new Editor (where the caret has been moved or some text has been changed) so I thought this might be a good application of the State monad and I have re-written most Editor-functions to now look like this:

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-符号内编写编辑动作.

This is pretty awesome, because it allows me to compose editing actions very easily within do-notation.

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

However, now I'm struggling to put this to use within an actual application. Say I want to use this Editor within an application that performs some IO. Say I want to manipulate an instance of Editor everytime the user presses the l key on the keyboard.

我将需要另一个State monad来代表持有Editor实例的整个应用程序状态,以及一个使用IO monad从键盘读取并调用moveHorizontally'的排序事件循环.通过修改其Editor来修改当前的AppState.

I would need to have another State monad representing the overall application state that holds an Editor instance and a sort-of event-loop that uses the IO monad to read from the keyboard and calls moveHorizontally' to modify the current AppState by modifying its Editor.

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

I've read up a bit on this topic and it seems like I need to use Monad Transformers to build a stack of monads with IO at the bottom. I've never used Monad Transformers before and I don't know what to do from here? I've also found out that the State monad already implements some functionality (it seems to be a special case of a Monad Transformer?) but I'm confused on how to use that?

推荐答案

首先,让我们备份一下.最好总是隔离问题.让纯函数与纯函数(状态-状态和IO-IO)分组.多个概念相互交织是烹饪意大利面条的必经之路.你不要那顿饭.

First, let's back up a bit. It's always best to have problems isolated. Let pure functions be grouped with pure functions, State - with State and IO - with IO. Intertwining multiple concepts is a certain recipe for cooking code-spaghetti. You don't want that meal.

话虽如此,让我们还原您拥有的纯函数并将它们分组在一个模块中.但是,我们将进行一些小的修改以使其符合Haskell约定-即,我们将更改参数顺序:

Having said that, let's restore the pure functions that you had and group them in a module. However we'll apply small modifications to make them conform to the Haskell conventions - namely, we'll change the parameter order:

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

现在,如果您真的想找回State API,可以在另一个模块中实现它很简单:

Now, if you really want to get your State API back, it's trivial to implement in another 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)

正如您现在所看到的,遵循标准约定使我们能够使用标准State实用程序,例如

As you see now, following the standard conventions allows us to use the standard State utilities like gets and modify to trivially lift the already implemented functions to the State monad.

但是,实际上上述实用程序也可用于StateT monad变压器,其中State实际上只是一个特例.因此,我们也可以以更通用的方式实现同​​一件事:

However, actually the mentioned utilities work for the StateT monad-transformer as well, of which State is actually just a special case. So we can just as well implement the same thing in a more general way:

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

如您所见,所有更改的只是类型签名.

As you see, all that's changed are the type signatures.

现在,您可以在变压器堆栈中使用这些常规功能.例如,

Now you can use those general functions in your transformer stack. E.g.,

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

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

Thus we've just achieved a granular isolation of concerns and a great flexibility of our codebase.

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

Now, of course, this makes a quite primitive example so far. Usually the final monad-transformer stack has more levels. E.g.,

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

要在所有这些级别之间切换,典型的工具集是" mtl库,它提供类型类以减少lift的使用.不过,我必须指出,并不是每个人(包括我自己)都喜欢"mtl",因为在减少代码量的同时,它引入了一定的歧义和推理的​​复杂性.我更喜欢显式使用lift.

To jump between all those levels the typical tool-set is the lift function or the "mtl" library, which provides type-classes to reduce the usage of lift. I have to mention though, that not everyone (myself including) is a fan of "mtl", because, while reducing the amount of code it introduces a certain ambiguity and reasoning complexity. I prefer to use lift explicitly.

变压器的要点是允许您以临时方式扩展现有的monad(变压器堆栈也为monad),并具有一些新功能.

The point of transformers is to allow you to extend an existing monad (transformer stack is a monad as well) with some new functionality in an ad-hoc way.

关于扩展应用程序状态的问题,您只需在堆栈中添加另一个StateT层即可:

As for your question about extending the app's state, you can simply add another StateT layer to your stack:

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

这篇关于州和IO Monads的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆