跳过 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
。
我知道你已经可以用 guard
和 when
做一些接近这个的事情了。可以不嵌套吗?
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"拆分为两个不同类型的变量(a
和result
):
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 -> b
和 a :: 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 -> b
和a :: 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
倾向于在较长的 运行 中减少头痛。
但是 ExceptT
比 ContT
更酷吗?
几乎没有。
您好,我正在寻找一种允许 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
。
我知道你已经可以用 guard
和 when
做一些接近这个的事情了。可以不嵌套吗?
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"拆分为两个不同类型的变量(a
和result
):
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 -> b
和a :: 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 -> b
和a :: 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
倾向于在较长的 运行 中减少头痛。
但是 ExceptT
比 ContT
更酷吗?
几乎没有。