callCC如何用严格的函数式语言实现?

时间:2013-12-21 04:07:00

标签: haskell callcc continuation

考虑函数quux的Haskell中的以下示例以及continuation monad和callCC的定义。

instance Monad (Cont r) where
    return x = cont ($ x)
    s >>= f  = cont $ \c -> runCont s $ \x -> runCont (f x) c

callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
callCC f = cont $ \h -> runCont (f (\a -> cont $ \_ -> h a)) h

quux :: Cont r Int
quux = callCC $ \k -> do
    let n = 5
    k n
    return 25

据我了解这个例子。 do块可以被认为是

k n >>= \_ -> return 25 == 
cont $ \c -> runCont (k n) $ \x -> runCont ((\_ -> return 25) x) c

我们可以从k的定义中看到\a -> cont $ \_ -> h a,在上面我们将\x -> runCont ((\_ -> return 25) x) c传递给用下划线忽略的参数。最终return 25被有效地“忽略”,因为下划线参数永远不会被使用,因此懒惰的评估永远不会被评估。

据我所知,callCC的实现从根本上取决于懒惰的评估。如何用严格(非懒惰)的函数式语言完成callCC

1 个答案:

答案 0 :(得分:7)

没有。 callcc的此实现不依赖于惰性求值。为了证明这一点,我将用严格的函数式语言实现它,并表明k n之后的任何事情都没有被执行。

我将使用的严格功能语言是JavaScript。由于JavaScript不是静态类型,因此您无需声明newtype。因此,我们首先在JavaScript中定义return monad的>>=Cont函数。我们将分别称这些函数为unitbind

function unit(a) {
    return function (k) {
        return k(a);
    };
}

function bind(m, k) {
    return function (c) {
        return m(function (a) {
            return k(a)(c);
        });
    };
}

接下来,我们按如下方式定义callcc

function callcc(f) {
    return function (c) {
        return f(function (a) {
            return function () {
                return c(a);
            };
        })(c);
    };
}

现在我们可以按如下方式定义quux

var quux = callcc(function (k) {
    var n = 5;

    return bind(k(n), function () {
        alert("Hello World!");
        return unit(25);
    });
});

请注意,我在alert的第二个参数中添加了bind来测试它是否已执行。现在,如果您致电quux(alert),它将显示5,但不会显示"Hello World!"。这证明bind的第二个参数从未执行过。请亲自查看demo

为什么会这样?让我们从quux(alert)开始倒退。通过beta减少它相当于:

(function (k) {
    var n = 5;

    return bind(k(n), function () {
        alert("Hello World!");
        return unit(25);
    });
})(function (a) {
    return function () {
        alert(a);
    };
})(alert);

通过beta再次降低它成为:

bind(function () {
    alert(5);
}, function () {
    alert("Hello World!");
    return unit(25);
})(alert);

接下来我们得到bind的beta缩减:

(function (c) {
    return (function () {
        alert(5);
    })(function (a) {
        return (function () {
            alert("Hello World!");
            return unit(25);
        })(a)(c);
    });
})(alert);

现在我们可以看到为什么"Hello World!"从未显示过。通过beta缩减,我们正在执行function () { alert(5); }。调用它的参数是这个函数的工作,但它永远不会。由于此执行停止并且永远不会显示"Hello World!"。总之:

callcc功能不依赖于延迟评估。

callcc创建的函数在调用k之后终止,不是因为延迟评估,而是因为调用k通过不调用它的第一个参数来打破链,因此立即返回。

这让我回到你的问题:

  

我们可以从k的定义中看到\a -> cont $ \_ -> h a,在上面我们将\x -> runCont ((\_ -> return 25) x) c传递给用下划线忽略的参数。最终return 25被有效地“忽略”,因为下划线参数永远不会被使用,因此懒惰的评估永远不会被评估。

你错了。正如您所看到的,k(\a -> cont $ \_ -> h a),函数(\x -> runCont ((\_ -> return 25) x) c)被传递到被k忽略的参数中。在那之前你是对的。但是,这并不意味着由于惰性求值而未评估return 25。它根本没有被评估,因为从不调用函数(\x -> runCont ((\_ -> return 25) x) c)。我希望能把事情搞清楚。