另一个答案很棒!我想谈谈延续解决方案究竟是如何工作的,所以我写了这个奇怪的大事。希望对您有所帮助。
第一幕:设下陷阱
我们从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 中夺取控制流,我们需要在这里劫持>>=。我们谨慎地猜测我们将需要自己的 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?。)
我们可以使用一个讨厌的技巧,即将“否定”和“肯定”分成两个不同类型的变量(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)。
一元实例同样困难:
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 -> m a) -> 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 往往会减少麻烦。
但是ExceptT 比ContT 酷吗?
几乎没有。