跳过 monad 中的剩余操作 - 例如 return

Skip the remaining actions in a monad - like return

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

例如,假设我正在使用 monadic 动作来处理副作用

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

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

   doSomeSideEffects r

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

我知道你已经可以用 guardwhen 做一些接近这个的事情了。可以不嵌套吗?

ExceptT 已经允许您退出正常流程,并且 return 可以早日获得结果。但是 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 >>= 似乎已经这样做了。至少它肯定在 monad 的可能性之内,但我不确定如何处理 monad 转换器。


这是一个更完整的示例。该系统应该执行"workflow"。每个步骤都会产生一个响应,这应该会停止整个工作流并进行响应 (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

如果您愿意包装您希望能够退出的范围,则可以使用 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,其中“早return”是延续。

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"

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

第一幕:陷阱已设

我们在 IO 的低洼平原开始我们的旅程,我们最喜欢的州 monad:

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

我们想向上攀登,从第一步开始就想办法return。我们的第一个尝试是引入某种类型可疑的转义机制:

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

我们祈祷,希望我们传递给escape的字符串最终成为IO String中的字符串,并思考究竟可以用什么来填充那些讨厌的问号。

在我们看来,如果我们希望从 IO monad 手中夺取控制流,就需要在这里劫持 >>=。我们谨慎地猜测我们将需要我们自己的 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"出现在"negative position":

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

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

我们可以使用一个卑鄙的技巧,即将"negatives"和"positives"拆分为两个不同类型的变量(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 -> 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 -> inner result构造内部结果的方法)、a2b :: a -> ba :: a

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

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

monadic 实例同样困难:

    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" 对 "everything after this point in the control flow." 的某些概念进行了编码,调用一次 运行 之后的所有内容。调用它三次 运行 调用它三次之后的所有内容。调用它零次...

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 块标记为需要提前退出会怎么样?与我们最初的转义机制非常相似的东西:

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 -> m a) -> m a 时几乎完全相同的信念飞跃。

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

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);当您激活延续时,它允许您立即 return 一个值。

  • 您可以通过延续实现很多事情,包括早期的 return 和异常、生成器,天知道还有什么。我们甚至还没有讨论定界延续(移位和重置)。它们代表了计算机编程结构的原始和基础。

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

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

可能不会。 ExceptT 倾向于在较长的 运行 中减少头痛。

但是 ExceptTContT 更酷吗?

几乎没有。