【问题标题】:Skip the remaining actions in a monad - like return跳过 monad 中的剩余操作 - 比如 return
【发布时间】:2017-04-19 20:32:40
【问题描述】:

您好,我正在寻找一种允许单子堆栈跳过剩余操作而不完全跳过的好方法。有点像 C 系列语言中的return

例如,假设我使用一元动作来产生副作用

type MyMonad = ??
doStuff :: MyMonad ()
doStuff = do
   r <- doSomething

   -- equivalent to if (r == "X") return; in C
   dontGoPastHereIf (r == "X")

   doSomeSideEffects r

所以我希望它只在某些条件下执行doSomeSideEffects

我知道你已经可以用guardwhen 做一些类似的事情了。不嵌套也可以吗?

ExceptT 已经允许您退出正常流程并以早期结果返回。但是对于ExceptT,错误/跳过将传播。我只想跳过本地函数中的其余步骤

doTwoSteps :: MyMonad ()
doTwoSteps = do
  -- if I used ExceptT, an error in the first function will skip the second.
  -- But I still want to do the second step here
  doStuff
  doStuff

似乎 bind &gt;&gt;= 已经这样做了。至少它肯定在单子的可能性范围内,但我不确定如何处理单子转换器。


这里有一个更完整的例子。该系统应该执行“工作流程”。每个步骤都可能导致响应,这应该会停止整个工作流程并做出响应 (ExceptT)。

可以通过传递ApplicationState 重新启动工作流。如果一个步骤之前有Continue,我们可以跳过该步骤的逻辑,但我们仍然需要执行下一步。

有没有更好的方法来做到这一点?是否有一些 monad 转换器或一种方法来定义我的 Flow monad,这样我就可以运行 checkShouldSkip 而无需传递动作?

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}

module Main where

import Control.Monad.Except (throwError, ExceptT)
import Control.Monad.State (gets, StateT, modify)
import Data.Text (Text)

data ApplicationState = ApplicationState
    { step1Result :: Maybe StepResult
    , step2Result :: Maybe StepResult
    } deriving (Show, Eq)

data StepResult
    = Stop
    | Continue
    deriving (Show, Eq)

type Flow a = StateT ApplicationState (ExceptT Text IO) a


flow :: Flow ()
flow = do
    step1
    step2

step1 :: Flow ()
step1 = do
    ms <- gets step1Result
    checkShouldSkip ms $ do
      info <- getStuffFromAServer
      let r = runSomeLogic info
      modify $ setStep1 $ Just r
      checkShouldRespond r

  where
    getStuffFromAServer = undefined
    runSomeLogic _ = undefined
    setStep1 r s = s { step1Result = r }


step2 :: Flow ()
step2 = do
    ms <- gets step2Result
    checkShouldSkip ms $ do
      -- this will run some different logic, eventually resulting in a step result
      r <- getStuffAndRunLogic
      modify $ setStep2 $ Just r
      checkShouldRespond r

  where
    getStuffAndRunLogic = undefined
    setStep2 r s = s { step2Result = r }


checkShouldSkip :: Maybe StepResult -> Flow () -> Flow ()
checkShouldSkip (Just Continue) _ = pure () -- skip the logic, continue
checkShouldSkip (Just Stop) _ = respond "Stop" -- skip the logic, stop everything
checkShouldSkip Nothing a = a -- run the action


checkShouldRespond :: StepResult -> Flow ()
checkShouldRespond Continue = pure ()
checkShouldRespond Stop = respond "Stop" -- if a response, stop all execution


-- rename because these aren't really errors, I just want to stop everything
respond :: Text -> Flow ()
respond t = throwError t

【问题讨论】:

  • 您可能正在寻找延续单子 ContContT
  • 在我看来,您要求的内容会破坏引用透明度 - 在 doTwoSteps 中,如果您将每个 doStuff 替换为其定义,您就改变了行为。
  • @Alec 你能告诉我如何在我的扩展示例中使用 ContT 吗?
  • @DavidFletcher 也许,我正在采取措施应对副作用。此外,在实际示例中,步骤是不同的。

标签: haskell monads monad-transformers


【解决方案1】:

另一个答案很棒!我想谈谈延续解决方案究竟是如何工作的,所以我写了这个奇怪的大事。希望对您有所帮助。

第一幕:设下陷阱

我们从IO 的低洼平原开始我们的旅程,这是我们最喜欢的状态单子:

module Lib where

step1 :: IO String
step1 = do
  print "step1 - A"
  print "step1 - B"
  pure "--step1 result--"

step2 :: String -> IO String
step2 input = do
  print input
  print "step2 - A"
  print "step2 - B"
  pure "--step2 complete--"

main :: IO ()
main = do
  result <- step1 >>= step2
  print "--done--"
  print result

我们想向上攀登,并找到一种从第一步尽早返回的方法。我们的第一个尝试是引入某种类型有问题的转义机制:

step1 :: (String -> ???) -> IO String
step1 escape = do
  print "step1 - A"
  escape "escaped!"
  print "step1 - B"
  pure "--step1 result--"

我们交叉手指,希望我们传递给escape 的字符串最终会成为IO String 中的字符串,并思考究竟是什么可以填补那些讨厌的问号。

在我们看来,如果我们想要从IO monad 中夺取控制流,我们需要在这里劫持&gt;&gt;=。我们谨慎地猜测我们将需要自己的 monad 转换器。

newtype StrangeT inner a =
  StrangeT { runStrangeT :: a -> ??? }

lift :: IO a -> StrangeT IO a
lift io =
  StrangeT (\trapDoor -> io >>= trapDoor)

escape :: a -> StrangeT IO a
escape a =
  StrangeT (\trapDoorA -> trapDoorA a)

step1 :: StrangeT IO String
step1 = do
  lift (print "step1 - A")
  escape "escaped!"
  lift (print "step1 - B")
  pure "--step1 result--"

我们可以将trapDoorA 视为一种由键保护的转义机制,该键是a 类型的任何值。一旦门打开,我们就会进入计算的下一步。

问号插入什么类型?我们有点把自己逼到了一个角落;为了编译这段代码,我们只能是:

newtype StrangeT inner a =
  StrangeT { runStrangeT :: (a -> inner a) -> inner a }

第二幕:陌生人仍然

我们现在需要实例化Monad (StrangeT inner)。不幸的是,我们将遇到一个大问题。 StrangeT 不是函子!

原因是“a”出现在“负位置”:

newtype StrangeT inner a =
  StrangeT { runStrangeT :: (a -> inner a) -> inner a }
                               -- ^^^^^^^
                               -- :(

(有关此主题的完整讨论,请参阅What is a contravariant functor?。)

我们可以使用一个讨厌的技巧,即将“否定”和“肯定”分成两个不同类型的变量(aresult):

newtype StrangeT result inner a =
  StrangeT { runStrangeT :: (a -> inner result) -> inner result }

lift :: IO a -> StrangeT whatever IO a
lift io = StrangeT (\trapDoor -> io >>= trapDoor)

escape :: a -> StrangeT whatever IO a
escape x = StrangeT (\trapDoor -> trapDoor x)

这使一切成为可能。我们现在可以实例化 Functor、Applicative 和 Monad。不过,我们不会试图找出答案,而是让类型检查器接管。任何类型检查的答案都是正确的。

instance Functor (StrangeT result inner) where
  fmap a2b (StrangeT strange) =
    StrangeT $ \trapDoor -> strange (\a -> trapDoor (a2b a))
             -- ^^^^^^^^
             -- b -> inner result

逻辑序列:

  • trapDoor 是构建inner result 值的唯一方法。

  • 它需要一个b类型的值。

  • 我们有a2b :: a -&gt; ba :: a

    instance Applicative (StrangeT result inner) where
      pure :: a -> StrangeT result inner a
      pure a = StrangeT $ \trapDoor -> trapDoor a
    
      (<*>) :: StrangeT result inner (a -> b) ->
               StrangeT result inner a ->
               StrangeT result inner b
      (StrangeT strangeA2B) <*> (StrangeT strangeA) =
    --          ^^^^^^^^^^                ^^^^^^^^
    --          (b -> inner result) -> inner result
    --                                    (a -> inner result) -> inner result
        StrangeT (\trapDoorB -> strangeA2B (\a2b -> strangeA (\a -> trapDoorB (a2b a))))
    --             ^^^^^^^^                 
    --             b -> inner result
    

逻辑序列:

  • 我们有trapDoorB :: b -&gt; inner result(构造内部结果的唯一方式)、a2b :: a -&gt; ba :: a

  • 我们需要构造一个StrangeT result inner b

  • 因此,我们必须在某个时候评估trapDoorB (a2b a)

一元实例同样困难:

    instance Monad (StrangeT result inner) where
      (StrangeT strangeA) >>= a2strangeB =
         --     ^^^^^^^^
         --     (a -> inner result) -> inner result
        StrangeT
          (\trapDoorB -> strangeA (\a -> let StrangeT strangeB = a2strangeB a in strangeB (\b -> trapDoorB b)))
         -- ^^^^^^^^^                                 ^^^^^^^^
         -- b -> inner result                         (b -> inner result) -> inner result

只有一种方法可以构建inner result,即通过trapDoorB,因此其他一切都是朝着这个单一目标构建的。

第三幕:失误

我们定义了一个 monad 转换器,但并不真正知道它的作用或工作原理!我们只是将看起来正确的类型拼凑在一起。

那么我们理应看到它的实际效果:

main :: IO ()
main = do
  _ <- runStrangeT (step1 >>= step2) (\a -> pure a)
  print "--done--"
  print result

这会产生以下输出:

λ> main
"step1 - A"
"step1 - B"
"--step1 result--"
"step2 - A"
"step2 - B"
"--done--"
"--step2 result--"

多么令人沮丧!我们是正确的起点。

但是,如果我们定义这个函数,就会发生一些奇怪的事情:

escape :: a -> StrangeT whatever IO a
escape x = StrangeT (\trapDoor -> trapDoor x)

escapeWeirdly :: a -> StrangeT whatever IO a
escapeWeirdly x = StrangeT (\trapDoor -> trapDoor x >> trapDoor x >> trapDoor x)

step1 :: StrangeT String IO String
step1 = do
  lift (print "step1 - A")
  escapeWeirdly "--step1 exit--"
  lift (print "step1 - B")
  pure "--step1 result--"

输出:

λ> main
"step1 - A"
"step1 - B"               <- trap door call #1
"--step1 result--"
"step2 - A"
"step2 - B"
"step1 - B"               <- trap door call #2
"--step1 result--"
"step2 - A"
"step2 - B"
"step1 - B"               <- trap door call #3
"--step1 result--"
"step2 - A"
"step2 - B"
"--done--"
"--step2 result--"

step2 运行三遍!似乎“trapDoor”编码了“控制流中此点之后的所有内容”的一些概念。调用一次会运行一次之后的所有内容。调用它 3 次会运行 3 次之后的所有内容。调用它零次...

cut :: a -> StrangeT a IO a
cut x = StrangeT (\_ -> return x)

step1 :: (String -> StrangeT String IO String) -> StrangeT String IO String
step1 exit = do
  lift (print "step1 - A")
  cut "--step1 exit--"
  lift (print "step1 - B")
  pure "--step1 result--"

main :: IO ()
main = do
  result <- runStrangeT (step1 undefined >>= step2) pure
  print "--done--"
  print result

输出:

λ> main
"step1 - A"
"--done--"
"--step1 exit--"

没有运行!这难以置信地接近我们的需要。

第四幕:成功及其代价

如果我们可以将 StrangeT 动作的 do-block 标记为需要提前退出会怎样?与我们最初的逃生机制非常相似:

step1 = withEscape $ \escape -> do
  lift (print "step1 - A")
  escape "--step1 exit--"
  lift (print "step1 - B")
  pure "--step1 result--"

withEscape 的作用是运行所写的 do 块,直到有人调用 escape,此时其余的计算被中止但是withEscape 之外的任何计算(即此处的第二步)按原样运行。

这个助手的类型必须是:

withEscape :: (??? -> StrangeT result inner a) -> StrangeT result inner a

几乎与我们从 m a(a -&gt; m a) -&gt; m a 时的信念飞跃完全相同。

由于我们将 String 传递给 escape 并将计算结果绑定到 do-block 的下一行,我们现在可以填写这些问号:

withEscape :: ((a -> StrangeT result inner whatever) -> StrangeT result inner a)
              -> StrangeT result inner a

狡猾的类型!我们将不得不再次按类型导航以找到定义:

-- We have to call f at some point, and trapDoorA
-- is the only way to construct an inner result.
withEscape f =
  StrangeT (\trapDoorA -> let StrangeT strangeA = f ??? in strangeA trapDoorA)

-- f is passed the early exit value
withEscape f =
  StrangeT (\trapDoorA ->
    let StrangeT strangeA = f (\a -> ???) in strangeA trapDoorA)

-- We need to construct a StrangeT value
withEscape f =
  StrangeT (\trapDoorA ->
    let StrangeT strangeA = f (\a -> StrangeT (\trapDoorWhatever -> ???)) in
    strangeA trapDoorA)

-- We are going to *ignore* the trapDoorWhatever
-- we are supposed to fall into, and *instead*
-- fall through our original trapDoorA.
withEscape f =
  StrangeT (\trapDoorA ->
    let StrangeT strangeA = f (\a -> StrangeT (\_ -> trapDoor a)) in
    strangeA trapDoorA)

这里发生的事情是我们偶然发现了一个解决方案,该解决方案为我们提供了两个活板门。我们选择从我们为自己建造的原始门掉下,而不是从第一扇门掉下来(这会使助手归结为pure 这样的东西,因为它会恢复正常的控制流)。电影Primer的影迷会认出这是原罪;普通人可能只是一脸困惑地看待这一切。

无论如何,这有效

step1 :: StrangeT String IO String
step1 =
  withEscape $ \escape -> do
    lift (print "step1 - A")
    escape "--step1 exit--"
    lift (print "step1 - B")
    pure "--step1 result--"

step2 :: String -> StrangeT String IO String
step2 result = do
  lift (print result)
  lift (print "step2 - A")
  lift (print "step2 - B")
  pure "--step2 result--"

main :: IO ()
main = do
  result <- runStrangeT (step1 >>= step2) pure
  print "--done--"
  print result

输出:

λ> main
"step1 - A"              <- early exit
"--step1 exit--"         <- step2 runs
"step2 - A"
"step2 - B"
"--done--"               <- back to main
"--step2 result--"

总结

  • 如电报所述,这是ContT monad,可以在transfomers package 中找到。我们一直称之为活板门的东西实际上是延续。

  • withEscape 更好地称为callCC(当前继续调用);它可以让您在调用callCC 时为当前延续命名(在我们的示例中为escape);当您激活延续时,它允许您立即返回一个值。

  • 你可以用延续来实现很多东西,包括早期返回和异常以及生成器,天知道还有什么。我们甚至还没有谈论定界延续(移位和重置)。它们代表了计算机编程结构的基本要素。

  • 有关详细信息,请参阅链接自Oleg Kiselyov's website 的系列论文。关于延续还有很多话要说。

你真的应该在现实生活中这样做吗?

可能不会。从长远来看,ExceptT 往往会减少麻烦。

但是ExceptTContT 酷吗?

几乎没有。

【讨论】:

  • 你错过了第四幕吗?
  • 这太好了,谢谢!喜欢 Primer 参考 :)
【解决方案2】:

如果您愿意包装您希望能够退出的范围,您可以使用 ExceptT 执行此操作:

type EarlyReturnT m a = ExceptT a m a

withEarlyReturn :: (Functor m) => EarlyReturnT m a -> m a
withEarlyReturn = fmap (either id id) . runExceptT

earlyReturn :: (Applicative m) => a -> EarlyReturnT m a
earlyReturn = ExceptT . pure . Left

例如:

doStuff :: Bool -> IO String
doStuff x = withEarlyReturn $ do
  lift $ putStrLn "hello"
  when x $ earlyReturn "beans"
  lift $ putStrLn "goodbye"
  return "eggs"

> doStuff False
hello
goodbye
"eggs"

> doStuff True
hello
"beans"

或者ContT,“提前回归”是一个延续。

type EarlyReturnT m a = ContT a m a

withEarlyReturn
  :: (Applicative m)
  => ((a -> EarlyReturnT m a) -> EarlyReturnT m a)
  -> m a
withEarlyReturn = flip runContT pure . callCC

doStuff :: Bool -> IO String
doStuff x = withEarlyReturn $ \ earlyReturn -> do
  lift $ putStrLn "hello"
  when x $ earlyReturn "beans"
  lift $ putStrLn "goodbye"
  return "eggs"

【讨论】:

  • 酷,谢谢。但是后来我遇到了 mtl 的限制,即堆栈中有多个例外,对吧?没有办法让它与我的扩展示例一起使用?
  • @SeanClarkHess:抱歉,我不使用mtl,所以我不确定解决方法,但也许我添加的ContT 示例会有所帮助。
  • @SeanClarkHess mtl 不会限制您的堆栈中只有一个 ExceptT。您只能使用MonadError-polymorphic 函数“访问”顶部的函数,但您可以拥有任意数量的函数。我特别提到这一点是因为创建自己的 ExceptT 新类型可能是明智的,它公开了一个 MonadEarlyExit 接口(你发明和实现)但将 MonadError 实现推迟到底层 monad。
  • 这是个好主意。我从未尝试过重新输入另一个转换器。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多