考虑函数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
?
答案 0 :(得分:7)
没有。 callcc
的此实现不依赖于惰性求值。为了证明这一点,我将用严格的函数式语言实现它,并表明k n
之后的任何事情都没有被执行。
我将使用的严格功能语言是JavaScript。由于JavaScript不是静态类型,因此您无需声明newtype
。因此,我们首先在JavaScript中定义return
monad的>>=
和Cont
函数。我们将分别称这些函数为unit
和bind
:
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)
。我希望能把事情搞清楚。