在尝试为ContT monad变换器构建一些直觉时,我(也许不出所料)发现自己很困惑。问题在于shiftT操作似乎没有任何用处。
首先是一个如何使用它的简单例子
shiftT $ \famr -> lift $ do
a <- calculateAFromEnvironment
famr a
只要它返回一些famr a
, m r
可能是一些更复杂的表达式。现在试图解释我对shiftT的直觉并没有增加任何东西:
-- inline shiftT
ContT (\f2 -> evalContT ((\f1 -> lift (do
a <- calculateAFromEnvironment
f1 a)) f2))
-- beta reduction
ContT (\f2 -> evalContT (lift (do
a <- calculateAFromEnvironment
f2 a)))
-- inline evalConT
ContT (\f2 -> runContT (lift (do
a <- calculateAFromEnvironment
f2 a)) return)
-- inline lift
ContT (\f2 -> runContT (ContT (\f3 -> (do
a <- calculateAFromEnvironment
f2 a) >>= f3)) return)
-- apply runConT
ContT (\f2 -> (\f3 -> (do
a <- calculateAFromEnvironment
f2 a) >>= f3) return)
-- beta reduce
ContT (\f2 -> (do
a <- calculateAFromEnvironment
f2 a) >>= return)
-- (>>= return) is identity
ContT $ \f2 -> do
a <- calculateAFromEnvironment
f2 a
原来我们可以直接构建ContT。
提问时间:是否存在shift / shiftT在cont / ContT上添加任何内容的情况?或者它们只是用于使代码更具可读性?
答案 0 :(得分:4)
在searching github Gurkenglas的建议之后,我发现了shiftT
resetT
resetT :: (Monad m) => ContT r m r -> ContT r' m r
resetT = lift . evalContT
shiftT :: (Monad m) => ((a -> m r) -> ContT r m r) -> ContT r m a
shiftT f = ContT (evalContT . f)
和shiftT
的用例,动机示例和语义!
这些功能非常简单。他们在this very nice explanation库中的定义很简单:
shiftT
但哲学和意义远远落后于一些直观的理解。所以我建议你阅读上面链接的解释。有时,实际上很容易定义的东西可以做一些复杂的事情。
根据以上链接的哈巴狗的解释改编文件:
callCC
shiftT
与resetT
类似,但激活延续时除外 由callCC
提供,它将运行到最近的封闭resetT
的末尾, 然后跳回到激活延续的点之后。 请注意,因为控件最终会返回到之后的点 subcontinuation已激活,您可以在中激活多次 相同的块。这与resetT
的延续不同,它延续了当前的延续 激活时执行路径。有关这些分隔子连续实际如何的示例,请参阅
shiftT
工作
resetT $ do alfa bravo x <- shiftT $ \esc -> do -- note: esc :: m Int, not a ContT charlie lift $ esc 1 delta lift $ esc 2 return 0 zulu x
创建一个范围,最终保证
alfa
的子连续 退出结束。考虑这个例子:bravo
这将:
执行
charlie
执行
x
执行
zulu 1
将
resetT
绑定为1,从而执行esc 1
- 之后
从
delta
结束,然后跳回x
执行
zulu 2
将
resetT
绑定为2,从而执行esc 2
- 之后
从
resetT
结束,然后跳回callCC
- 醇>
逃离
resetT
,导致其产生0因此,与
constexpr
的延续不同,这些子连续最终将会延续 在它们被激活之后返回到它之后 最近decltype(...){}
。
答案 1 :(得分:2)
你是对的delimited continuations可以使用无限延续来表达。因此,shiftT
和resetT
的定义始终只能使用ContT
来描述。但是:
本质上,continuation允许将程序内部翻出来:当reset
调用传递的函数时,由shift
分隔的块被挤压到程序的内部部分。 (在无限延续的情况下,整个执行上下文被挤入内部,这就是使它们如此奇怪的原因。)
我们举几个例子:
import Data.List
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Cont
test0 :: Integer
test0 = evalCont . reset $ do
return 0
如果我们reset
没有shift
,那只是一个纯粹的计算,没什么特别的。上述函数只返回0
。
现在让我们使用它们:
test1 :: Integer
test1 = evalCont . reset $ do
r <- shift $ \esc -> do
let x = esc 2
y = esc 3
return $ x * y
return $ 1 + r
这变得更有趣。
shift
和reset
之间的代码实际上被压缩到esc
的调用中,在这个简单的示例中它只是return $ 1 + r
。当我们调用esc
时,将执行整个计算,其结果将成为esc
调用的结果。我们这样做了两次,所以基本上我们两次调用shift
和reset
之间的所有内容。整个计算的结果是result $ x * y
,shift
调用的结果。
从某种意义上说,shift
块成为计算的外部部分,reset
和shift
之间的块成为计算的内部部分。
到目前为止一切顺利。但是如果我们两次调用shift
,就会变得更加艰巨,就像在这个代码示例中一样:
list2 :: [(Int, String)]
list2 = evalCont . reset $ do
x <- shift $ \yieldx ->
return $ concatMap yieldx [1, 2, 3]
y <- shift $ \yieldy ->
return $ concatMap yieldy ["a", "b", "c"]
return [(x, y)]
这就是它产生的东西(对那些想要试图把它弄清楚作为练习的人来说是隐藏的):
[(1,"a"),(1,"b"),(1,"c"),(2,"a"),(2,"b"),(2,"c"),(3,"a"),(3,"b"),(3,"c")]
现在发生的事情是该程序内外输出两次:
x <- shift ...
块之外的所有内容都绑定到yieldx
来电,包括下一个shift
。计算结果是x <- shift ...
块的结果。y <- shift ...
内的第二个yieldx
时,计算的其余部分再次绑定到yieldy
调用。此内部计算的结果是y <- shift ...
块的结果。因此,在x <- shift
中,我们为三个参数中的每一个运行剩余的计算,并且在每个参数中,我们对其他三个参数执行类似的操作。结果是两个列表的笛卡尔积,因为我们基本上执行了两个嵌套循环。
同样的事情适用于shiftT
和resetT
,只是增加了副作用。例如,如果我们想调试实际发生的事情,我们可以在IO
monad中运行上面的代码并打印调试语句:
list2' :: IO [(Int, String)]
list2' = evalContT . resetT $ do
x <- shiftT $ \yield ->
lift . liftM concat . mapM (\n -> print n >> yield n) $ [1, 2, 3]
y <- shiftT $ \yield ->
lift . liftM concat . mapM (\n -> print n >> yield n) $ ["a", "b", "c"]
return [(x, y)]