不同类型短路的状态计算(也许,要么)

jak*_*iel 4 haskell functional-programming imperative-programming state-monad monad-transformers

我试图找到将以下有状态命令式代码段转换为纯函数表示的最优雅方式(最好在 Haskell 中使用其 Monad 实现提供的抽象)。但是,我还不擅长使用转换器等组合不同的 monad。在我看来,在学习如何自己做时,分析他人对此类任务的看法最有帮助。命令式代码:

while (true) {
  while (x = get()) { // Think of this as returning Maybe something
    put1(x) // may exit and present some failure representation
  }
  put2() // may exit and present some success representation
}
Run Code Online (Sandbox Code Playgroud)

get返回时Nothing我们需要继续执行put2,当get返回时Just x我们希望x被传递给put1并且仅在put1失败或循环时才短路。基本上put1并且put2可能会终止整个事情或移动到以下语句以某种方式改变底层状态。get可以成功并调用put1和循环或失败并继续put2

我的想法是:

forever $ do
  forever (get >>= put1)
  put2
Run Code Online (Sandbox Code Playgroud)

为什么我要寻找这样的东西是因为(get >>= put1)只要get没有任何东西可以返回或put1终止,就可以简单地短路。类似地put2终止外循环。但是,我不确定如何将其State与必要的Maybe和/或Either实现这一目标相结合。

我认为使用转换器来组合State其他 monad 是必要的,因此代码很可能不会那么简洁。但我想它也可能不会更糟。

欢迎任何关于如何优雅地实现翻译的建议。这不同于“与不同类型的中断有状态循环使用,避免明确的控制流” ifwhenwhile和而试图鼓励使用MaybeEither或者一些其他方便的>>=语义。此外,总是有一种直接的方法可以将代码转换为功能性的代码,但它很难被认为是优雅的。

Cir*_*dec 5

您正在寻找EitherTExceptT。它增加了两种返回变压器堆栈的方法。计算可以是return athrowError e。错误和返回之间有两个区别。错误保留在 上,Left并在 上返回Right。当您>>=遇到错误时,它会短路。

newtype EitherT e m a = EitherT { runEitherT :: m (Either e a) }

return :: a -> EitherT e m a
return a = EitherT $ return (Right a)

throwError :: e -> EitherT e m a
throwError e = EitherT $ return (Left a)
Run Code Online (Sandbox Code Playgroud)

我们还将使用名称left = throwErrorright = return

错误Left不会继续,我们将使用它们来表示退出循环。我们将使用该类型EitherT r m ()来表示一个循环,该循环要么以中断结果停止,要么以Left r继续Right ()。这几乎完全相同forever,除了我们解开EitherT并去掉Left返回值周围的 。

import Control.Monad
import Control.Monad.Trans.Either

untilLeft :: Monad m => EitherT r m () -> m r
untilLeft = liftM (either id id) . runEitherT . forever   
Run Code Online (Sandbox Code Playgroud)

在充实您的示例之后,我们将回到如何使用这些循环。

由于您希望看到几乎所有逻辑都消失,因此我们也将EitherT用于其他所有逻辑。获取数据的计算要么是要么Done返回数据。

import Control.Monad.Trans.Class
import Control.Monad.Trans.State

data Done = Done       deriving Show

-- Gets numbers for a while.
get1 :: EitherT Done (State Int) Int
get1 = do
    x <- lift get
    lift . put $ x + 1
    if x `mod` 3 == 0
    then left Done
    else right x
Run Code Online (Sandbox Code Playgroud)

放置数据的第一个计算是 aFailure或返回。

data Failure = Failure deriving Show

put1 :: Int -> EitherT Failure (State Int) ()
put1 x = if x `mod` 16 == 0
         then left Failure
         else right ()
Run Code Online (Sandbox Code Playgroud)

放置数据的第二个计算是 aSuccess或返回。

data Success = Success deriving Show

put2 :: EitherT Success (State Int) ()
put2 = do 
        x <- lift get
        if x `mod` 25 == 0
        then left Success
        else right ()
Run Code Online (Sandbox Code Playgroud)

对于您的示例,我们需要组合两个或多个以不同方式异常停止的计算。我们将用两个嵌套的EitherTs来表示它。

EitherT o (EitherT i m) r
Run Code Online (Sandbox Code Playgroud)

外部EitherT是我们目前正在操作的那个。我们可以通过在每个周围添加一个额外的层来将 an 转换EitherT o m a为 an 。EitherT o (EitherT i m) aEitherTm

over :: (MonadTrans t, Monad m) => EitherT e m a -> EitherT e (t m) a
over = mapEitherT lift
Run Code Online (Sandbox Code Playgroud)

内层EitherT将像变压器堆栈中的任何其他底层 monad 一样被处理。我们可以liftEitherT i m aEitherT o (EitherT i m) a

我们现在可以构建一个成功或失败的整体计算。会打破当前循环的计算被运行over。会破坏外循环的计算是lifted。

example :: EitherT Failure (State Int) Success
example =
    untilLeft $ do
        lift . untilLeft $ over get1 >>= lift . put1
        over put2
Run Code Online (Sandbox Code Playgroud)

总的来说,在最里面的循环中Failurelift编辑了两次。这个例子很有趣,可以看到一些不同的结果。

main = print . map (runState $ runEitherT example) $ [1..30]
Run Code Online (Sandbox Code Playgroud)

如果EitherT有一个MFunctor实例,over就是hoist lift,这是一个经常使用的模式,值得拥有一个深思熟虑的名字。顺便说一句,我使用EitherToverExceptT主要是因为它的名称加载较少。MFunctor对我来说,无论哪个先提供实例,最终都会作为 monad 转换器获胜。