今天的淋浴思考是,有没有一种方法可以将双重否定的消除解释为程序?
((a -> Void) -> Void) -> a
(这里 Void表示空类型)
乍一看,这似乎很荒谬:人们如何期望仅仅从一个类型为(a -> Void) -> Void ,甚至不返回类型为a 的函数中产生一个完全任意类型的返回值a ?
一个提示来自于Peirce定律,也被称为 "调用-当前延续",它有一个类似的形式:
callCC :: ((a -> Cont r) -> Cont a) -> Cont a
(我们用 Cont来允许代码在Haskell程序中被测试)。
Peirce定律让程序完全控制自己的状态(即堆栈框架)。应用 callCC对一个被调用者g :: (a -> Cont r) -> Cont a ,赋予了它非局部跳转的能力:
example :: Cont String
example =
callCC $ \ yield -> do
yield "escaped"
pure "stayed"
这将返回"escaped" ,而不是"stayed" 。这就好像内层函数向外层范围执行goto/return/break/longjmp ,沿途传递结果。
事实证明,doubleNegationElimination 可以用callCC 来表示:
doubleNegationElimination :: ((a -> Cont Void) -> Cont Void) -> Cont a
doubleNegationElimination doubleNegation =
callCC $ \ yield -> do
void <- doubleNegation $ \ x ->
yield x -- goodbye!
absurd void
doubleNegation :: (a -> Cont Void) -> Cont Void由此,我们可以看到漏洞:通过在a -> Cont Void ,程序可以捕获并通过非本地跳转将结果发送给调用者。剩下的部分涉及 absurd只是为了满足类型检查器的要求。
例如,如果doubleNegation 是这样构造的:
doubleNegation :: (String -> Cont Void) -> Cont Void
doubleNegation negation = negation "strings do exist"
那么doubleNegationElimination 将能够通过捕获值来体现证据"strings do exist" ,然后立即跳转到外层作用域,避免因评估Void 而产生的崩溃。
话说回来,callCC 的功能非常强大。你可以用它们来实现Python风格的生成器!
import Control.Monad.Cont (MonadCont, MonadIO, (<=<),
callCC, forever, liftIO, runContT)
import Data.Foldable (for_)
import Data.IORef (IORef)
import Data.Void (Void, absurd)
import qualified Data.IORef as IO
main :: IO ()
main = runContT generatorExample pure
generatorExample :: (MonadCont m, MonadIO m) => m ()
generatorExample = callCC $ \ stop -> do
g <- newGenerator [0 .. 9 :: Integer]
forever $ do
result <- g
case result of
Nothing -> stop ()
Just x -> liftIO (print x)
newGenerator :: (MonadCont m, MonadIO m) => [a] -> m (m (Maybe a))
newGenerator xs = callCC' $ \ ret -> do
-- rNext stores the function used to jump back into the generator
rNext <- newIORef ($ Nothing)
-- rYield stores the function used to jump out of the generator
rYield <- newIORef <=< callCC' $ \ next -> do
writeIORef rNext next
ret $ do
-- the generator itself is just a function that saves the current
-- continuation and jumps back into the generator via rNext
callCC' $ \ yield -> do
readIORef rNext >>= ($ yield)
for_ xs $ \ i -> do
-- save the current continuation then jump out of the generator
writeIORef rYield <=< callCC' $ \ next -> do
writeIORef rNext next
readIORef rYield >>= ($ Just i)
readIORef rYield >>= ($ Nothing)
-- | This is just double negation elimination under a different name, which
-- turns out to be a little more convenient than 'callCC' here.
callCC' :: MonadCont m => ((a -> m Void) -> m Void) -> m a
callCC' action = callCC ((absurd <$>) . action)
newIORef :: MonadIO m => a -> m (IORef a)
newIORef value = liftIO (IO.newIORef value)
readIORef :: MonadIO m => IORef a -> m a
readIORef ref = liftIO (IO.readIORef ref)
writeIORef :: MonadIO m => IORef a -> a -> m ()
writeIORef ref value = liftIO (IO.writeIORef ref value)