【发布时间】:2021-01-18 10:09:58
【问题描述】:
我对代数数据类型几乎没有经验,因为我使用的语言没有原生支持。通常可以使用延续传递风格来获得远程相似的体验,但 CPS 编码类型的处理不太舒服。
考虑到这一点,为什么像 Parsec 这样的库会使用 CPS?
newtype ParsecT s u m a
= ParsecT {unParser :: forall b .
State s u
-> (a -> State s u -> ParseError -> m b) -- consumed ok
-> (ParseError -> m b) -- consumed err
-> (a -> State s u -> ParseError -> m b) -- empty ok
-> (ParseError -> m b) -- empty err
-> m b
}
一个线索是 try 解析器,它通过在两种情况下传递 empty error 延续来排除 consumed error 情况:
try :: ParsecT s u m a -> ParsecT s u m a
try p =
ParsecT $ \s cok _ eok eerr ->
unParser p s cok eerr eok eerr
-- ^^^^ ^^^^
这是可能的,因为两个延续 cerr 和 eerr 具有相同的类型,只是它们的位置不同,这让我想起了结构类型。虽然我认为你不能用 ADT 做到这一点,但可能有一种方法可以用它们实现相同的行为。除此之外,单个组合器无法证明依赖 CPS 的影响深远的决定是合理的。那么为什么会做出这个决定呢?
【问题讨论】:
-
我认为这里有性能优势 - 减少中间结构。就表现力而言,我认为它们是“平等的”。
-
我相信这是一个古老的设计决定,最初是出于性能原因,但在现代 GHC 上,非 CPS 解析器比 CPS 解析器快得多。对于可恢复解析器,尽管 CPS 提供了一个简单的实现。
-
@AndrásKovács 你有一些方便的链接来比较两者的基准比较吗?我有兴趣阅读更多内容。
-
@AndrásKovács,我记得非常现代的
serialise包使用 CPS,但采取了其他步骤来避免过度依赖优化器(也许避开专业化?)。再说一次,解析和反序列化不一样。
标签: haskell functional-programming algebraic-data-types continuation-passing