双重否定消除法的程序解释

277 阅读1分钟

今天的淋浴思考是,有没有一种方法可以将双重否定的消除解释为程序?

((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)