10

有人可以举一个简单的例子,其中 state monad 比直接传递 state 更好吗?

bar1 (Foo x) = Foo (x + 1)

对比

bar2 :: State Foo Foo
bar2 = do
  modify (\(Foo x) -> Foo (x + 1))
  get
4

3 回答 3

17

状态传递通常是乏味的、容易出错的并且阻碍重构。例如,尝试按后序标记二叉树或玫瑰树:

data RoseTree a = Node a [RoseTree a] deriving (Show)

postLabel :: RoseTree a -> RoseTree Int
postLabel = fst . go 0 where
  go i (Node _ ts) = (Node i' ts', i' + 1) where

    (ts', i') = gots i ts

    gots i []     = ([], i)
    gots i (t:ts) = (t':ts', i'') where
      (t', i')   = go i t
      (ts', i'') = gots i' ts

在这里,我必须以正确的顺序手动标记状态,传递正确的状态,并且必须确保标签和子节点在结果中的顺序正确(请注意,天真的使用foldrorfoldl子节点可能很容易导致不正确的行为)。

另外,如果我尝试将代码更改为预购,我必须进行容易出错的更改:

preLabel :: RoseTree a -> RoseTree Int
preLabel = fst . go 0 where
  go i (Node _ ts) = (Node i ts', i') where -- first change

    (ts', i') = gots (i + 1) ts -- second change

    gots i []     = ([], i)
    gots i (t:ts) = (t':ts', i'') where
      (t', i')   = go i t
      (ts', i'') = gots i' ts

例子:

branch = Node ()
nil  = branch []
tree = branch [branch [nil, nil], nil]
preLabel tree == Node 0 [Node 1 [Node 2 [],Node 3 []],Node 4 []]
postLabel tree == Node 4 [Node 2 [Node 0 [],Node 1 []],Node 3 []]

对比状态单子解决方案:

import Control.Monad.State
import Control.Applicative

postLabel' :: RoseTree a -> RoseTree Int
postLabel' = (`evalState` 0) . go where
  go (Node _ ts) = do
    ts' <- traverse go ts
    i   <- get <* modify (+1)
    pure (Node i ts')

preLabel' :: RoseTree a -> RoseTree Int
preLabel' = (`evalState` 0) . go where
  go (Node _ ts) = do
    i   <- get <* modify (+1)
    ts' <- traverse go ts
    pure (Node i ts')

这段代码不仅更简洁、更容易正确编写,而且导致订单前或订单后标签的逻辑更加透明。


PS:奖金适用风格:

postLabel' :: RoseTree a -> RoseTree Int
postLabel' = (`evalState` 0) . go where
  go (Node _ ts) =
    flip Node <$> traverse go ts <*> (get <* modify (+1))

preLabel' :: RoseTree a -> RoseTree Int
preLabel' = (`evalState` 0) . go where
  go (Node _ ts) =
    Node <$> (get <* modify (+1)) <*> traverse go ts
于 2015-07-17T14:00:08.377 回答
6

作为我上面评论State的示例,您可以使用monad编写代码,例如

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

import Data.Text (Text)
import qualified Data.Text as Text
import Control.Monad.State

data MyState = MyState
    { _count :: Int
    , _messages :: [Text]
    } deriving (Eq, Show)
makeLenses ''MyState

type App = State MyState

incrCnt :: App ()
incrCnt = modify (\my -> my & count +~ 1)

logMsg :: Text -> App ()
logMsg msg = modify (\my -> my & messages %~ (++ [msg]))

logAndIncr :: Text -> App ()
logAndIncr msg = do
    incrCnt
    logMsg msg

app :: App ()
app = do
    logAndIncr "First step"
    logAndIncr "Second step"
    logAndIncr "Third step"
    logAndIncr "Fourth step"
    logAndIncr "Fifth step"

请注意,使用额外的运算符 fromControl.Lens还可以让您编写incrCntand logMsgas

incrCnt = count += 1

logMsg msg = messages %= (++ [msg])

State这是与库结合使用的另一个好处lens,但为了比较,我在本例中没有使用它们。要编写上面的等效代码,只传递参数,它看起来更像

incrCnt :: MyState -> MyState
incrCnt my = my & count +~ 1

logMsg :: MyState -> Text -> MyState
logMsg my msg = my & messages %~ (++ [msg])

logAndIncr :: MyState -> Text -> MyState
logAndIncr my msg =
    let incremented = incrCnt my
        logged = logMsg incremented msg
    in logged

在这一点上还不算太糟糕,但是一旦我们进入下一步,我想你会看到代码重复的真正来源:

app :: MyState -> MyState
app initial =
    let first_step  = logAndIncr initial     "First step"
        second_step = logAndIncr first_step  "Second step"
        third_step  = logAndIncr second_step "Third step"
        fourth_step = logAndIncr third_step  "Fourth step"
        fifth_step  = logAndIncr fourth_step "Fifth step"
    in fifth_step

将其包装在一个Monad实例中的另一个好处是您可以使用它的全部Control.Monad功能Control.Applicative

app = mapM_ logAndIncr [
    "First step",
    "Second step",
    "Third step",
    "Fourth step",
    "Fifth step"
    ]

与静态值相比,它在处理运行时计算的值时具有更大的灵活性。

手动状态传递和使用 monad 之间的区别State只是Statemonad 是对手动过程的抽象。它也恰好适合其他几个广泛使用的更通用的抽象,如MonadApplicativeFunctor和其他一些抽象。如果您还使用StateT转换器,那么您可以使用其他 monad 组合这些操作,例如IO. 你能在没有Stateand的情况下完成所有这些StateT吗?当然可以,并且没有人阻止您这样做,但关键是State抽象出这种模式并让您可以访问一个包含更通用工具的巨大工具箱。此外,对上述类型的小修改使相同的函数在多个上下文中工作:

incrCnt :: MonadState MyState m => m ()
logMsg :: MonadState MyState m => Text -> m ()
logAndIncr :: MonadState MyState m => Text -> m ()

这些现在将与App, 或StateT MyState IO, 或任何其他带有MonadState实现的 monad 堆栈一起使用。它使得它比简单的参数传递更可重用,这只能通过抽象是StateT.

于 2015-07-17T14:37:41.813 回答
1

根据我的经验,许多 Monad 的要点在您进入更大的示例之前并没有真正起作用,所以这里有一个使用State(well, StateT ... IO) 来解析传入 Web 服务的请求的示例。

模式是可以使用一堆不同类型的选项调用此 Web 服务,尽管除了其中一个选项之外的所有选项都具有不错的默认值。如果我收到带有未知键值的传入 JSON 请求,我应该使用适当的消息中止。我使用状态来跟踪当前配置是什么,以及 JSON 请求的其余部分是什么,以及一堆访问器方法。

(基于当前生产中的代码,所有内容的名称都发生了变化,并且该服务实际执行的细节被掩盖了)

{-# LANGUAGE OverloadedStrings #-}

module XmpConfig where

import Data.IORef
import Control.Arrow (first)
import Control.Monad
import qualified Data.Text as T
import Data.Aeson hiding ((.=))
import qualified Data.HashMap.Strict as MS
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (execStateT, StateT, gets, modify)
import qualified Data.Foldable as DF
import Data.Maybe (fromJust, isJust)

data Taggy = UseTags Bool | NoTags
newtype Locale = Locale String

data MyServiceConfig = MyServiceConfig {
    _mscTagStatus :: Taggy
  , _mscFlipResult :: Bool
  , _mscWasteTime :: Bool
  , _mscLocale :: Locale
  , _mscFormatVersion :: Int
  , _mscJobs :: [String]
  }

baseWebConfig :: IO (IORef [String], IORef [String], MyServiceConfig)
baseWebConfig = do
  infoRef <- newIORef []
  warningRef <- newIORef []
  let cfg = MyServiceConfig {
        _mscTagStatus = NoTags
        , _mscFlipResult = False
        , _mscWasteTime = False
        , _mscLocale = Locale "en-US"
        , _mscFormatVersion = 1
        , _mscJobs = []
        }
  return (infoRef, warningRef, cfg)

parseLocale :: T.Text -> Maybe Locale
parseLocale = Just . Locale . T.unpack  -- The real thing does more

parseJSONReq :: MS.HashMap T.Text Value ->
                IO (IORef [String], IORef [String], MyServiceConfig)
parseJSONReq m = liftM snd
                 (baseWebConfig >>= (\c -> execStateT parse' (m, c)))
  where
    parse' :: StateT (MS.HashMap T.Text Value,
                      (IORef [String], IORef [String], MyServiceConfig))
              IO ()
    parse' = do
      let addWarning s = do let snd3 (_, b, _) = b
                            r <- gets (snd3 . snd)
                            liftIO $ modifyIORef r (++ [s])
          -- These two functions suck a key/value off the input map and
          -- pass the value on to the handler "h"
          onKey      k h = onKeyMaybe k $ DF.mapM_ h
          onKeyMaybe k h = do myb <- gets fst
                              modify $ first $ MS.delete k
                              h (MS.lookup k myb)
          -- Access the "lns" field of the configuration
          config setter = modify (\(a, (b, c, d)) -> (a, (b, c, setter d)))

      onKey "tags" $ \x -> case x of
        Bool True ->       config $ \c -> c {_mscTagStatus = UseTags False}
        String "true" ->   config $ \c -> c {_mscTagStatus = UseTags False}
        Bool False ->      config $ \c -> c {_mscTagStatus = NoTags}
        String "false" ->  config $ \c -> c {_mscTagStatus = NoTags}
        String "inline" -> config $ \c -> c {_mscTagStatus = UseTags True}
        q -> addWarning ("Bad value ignored for tags: " ++ show q)
      onKey "reverse" $ \x -> case x of
        Bool r -> config $ \c -> c {_mscFlipResult = r}
        q -> addWarning ("Bad value ignored for reverse: " ++ show q)
      onKey "spin" $ \x -> case x of
        Bool r -> config $ \c -> c {_mscWasteTime = r}
        q -> addWarning ("Bad value ignored for spin: " ++ show q)
      onKey "language" $ \x -> case x of
        String s | isJust (parseLocale s) ->
          config $ \c -> c {_mscLocale = fromJust $ parseLocale s}
        q -> addWarning ("Bad value ignored for language: " ++ show q)
      onKey "format" $ \x -> case x of
        Number 1 -> config $ \c -> c {_mscFormatVersion = 1}
        Number 2 -> config $ \c -> c {_mscFormatVersion = 2}
        q -> addWarning ("Bad value ignored for format: " ++ show q)
      onKeyMaybe "jobs" $ \p -> case p of
        Just (Array x) -> do q <- parseJobs x
                             config $ \c -> c {_mscJobs = q}
        Just (String "test") ->
          config $ \c -> c {_mscJobs = ["test1", "test2"]}
        Just other -> fail $ "Bad value for jobs: " ++ show other
        Nothing    -> fail "Missing value for jobs"
      m' <- gets fst
      unless (MS.null m') (fail $ "Unrecognized key(s): " ++ show (MS.keys m'))

    parseJobs :: (Monad m, DF.Foldable b) => b Value -> m [String]
    parseJobs = DF.foldrM (\a b -> liftM (:b) (parseJob a)) []
    parseJob :: (Monad m) => Value -> m String
    parseJob (String s) = return (T.unpack s)
    parseJob q = fail $ "Bad job value: " ++ show q
于 2015-07-17T14:18:34.967 回答