haskell - 状态和错误 monad 堆栈,错误时状态回滚

标签 haskell state-monad

我有一个问题,我将通过以下示例来说明:

假设我想做一些可以产生结果或错误的计算,同时携带一个状态。为此,我有以下 monad 堆栈:

import Control.Monad.Trans.State ( get, modify, State )
import Control.Monad.Trans.Except ( catchE, throwE, ExceptT )

type MyMonad a = ExceptT String (State [Int]) a

因此,状态是一个整数列表,错误是字符串,计算可以产生任何类型“a”的值。我可以做这样的事情:

putNumber :: Int -> MyMonad ()
putNumber i = lift $ modify (i:)

现在,假设我定义了一个函数,将最后一个数字的一​​半添加到状态中:

putHalf :: MyMonad ()
putHalf = do
  s <- lift get
  case s of
    (x:_) -> if even x then putNumber (div x 2) else throwE "Number can't be halved"
    [] -> throwE "The state is empty"

使用putHalf要么向状态添加一个数字并返回 void,要么产生这两个错误中的任何一个。

如果发生错误,我希望能够调用替代函数。我知道我可以通过 catchE 通过执行以下操作来实现此目的:

putWithAlternative :: MyMonad ()
putWithAlternative = putHalf `catchE` (\_ -> putNumber 12)

在这种情况下,如果 putHalf由于任何原因失败,数字 12 将被添加到状态中。到目前为止一切都很好。但是,我可以定义一个名为 putHalf 的函数。两次:

putHalfTwice :: MyMonad ()
putHalfTwice = putHalf >> putHalf

问题是,例如,如果状态仅包含数字 2,则第一次调用 putHalf会成功并修改状态,但第二个会失败。我需要putHalfTwice执行两次调用并修改状态两次,或者根本不修改状态并保持原样。我无法使用catchEputWithAlternative ,因为状态在第一次调用时仍然被修改。

我知道 Parsec 库通过其 <|> 来做到这一点和try运营商。我怎样才能自己定义这些呢?是否有任何已经定义的 monad 转换器可以实现此目的?

最佳答案

如果在您的问题域中,失败永远不应该修改状态,那么最简单的方法就是反转各层:

type MyMonad' a = StateT [Int] (Except String) a

你原来的 monad 同构于:

s -> (Either e a, s)

所以它总是返回一个新的状态,无论成功还是失败。这个新的 monad 同构于:

s -> Either e (a, s)

因此它要么失败,要么返回一个新状态。

以下程序从 putHalfTwice 恢复,而不会破坏状态:

import Control.Monad.Trans
import Control.Monad.Trans.State
import Control.Monad.Trans.Except

type MyMonad' a = StateT [Int] (Except String) a

putNumber :: Int -> MyMonad' ()
putNumber i = modify (i:)

putHalf :: MyMonad' ()
putHalf = do
  s <- get
  case s of
    (x:_) -> if even x then putNumber (div x 2) else lift $ throwE "Number can't be halved"
    [] -> lift $ throwE "the state is empty"

putHalfTwice :: MyMonad' ()
putHalfTwice = putHalf >> putHalf

foo :: MyMonad' ()
foo = liftCatch catchE putHalfTwice (\_ -> putNumber 12)

main :: IO ()
main = do
  print $ runExcept (runStateT foo [2])

否则,如果您希望回溯是可选的,那么您可以编写自己的 try 来捕获、恢复状态并重新抛出:

try :: MyMonad a -> MyMonad a
try act = do
  s <- lift get
  act `catchE` (\e -> lift (put s) >> throwE e)

然后:

import Control.Monad.Trans
import Control.Monad.Trans.State
import Control.Monad.Trans.Except

type MyMonad a = ExceptT String (State [Int]) a

putNumber :: Int -> MyMonad ()
putNumber i = lift $ modify (i:)

putHalf :: MyMonad ()
putHalf = do
  s <- lift get
  case s of
    (x:_) -> if even x then putNumber (div x 2) else throwE "Number can't be halved"
    [] -> throwE "The state is empty"

putHalfTwice :: MyMonad ()
putHalfTwice = putHalf >> putHalf

try :: MyMonad a -> MyMonad a
try act = do
  s <- lift get
  act `catchE` (\e -> lift (put s) >> throwE e)

foo :: MyMonad ()
foo = putHalfTwice `catchE` (\_ -> putNumber 12)

bar :: MyMonad ()
bar = try putHalfTwice `catchE` (\_ -> putNumber 12)

main :: IO ()
main = do
  print $ runState (runExceptT foo) [2]
  print $ runState (runExceptT bar) [2]

关于haskell - 状态和错误 monad 堆栈,错误时状态回滚,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65442421/

相关文章:

haskell - 让 Haskell 势在必行

Haskell - 混合状态计算

haskell - HTF 不测试 TH 生成的 Prop

haskell - 文本编码问题

haskell - 为什么 "fmap (replicate 3) Just"的类型为 "a -> [Maybe a]",在 Haskell 中

haskell - 简单 Haskell 函数中不断出现(使用FlexibleContexts 来允许此操作)错误

haskell - 状态 Monad 的范围

Haskell:知道变量的类型是否派生Eq

haskell - 写入文件时如何压缩输出?

haskell - 使用状态 monad 时出现意外行为 - 状态包含在列表中