【问题标题】:Sharing vs. non-sharing fixed-point combinator共享与非共享定点组合器
【发布时间】:2018-12-11 00:45:15
【问题描述】:

这是 Haskell 中定点组合器的通常定义:

fix :: (a -> a) -> a
fix f = let x = f x in x

https://wiki.haskell.org/Prime_numbers 上,它们定义了不同的定点组合器:

_Y   :: (t -> t) -> t
_Y g = g (_Y g)                -- multistage, non-sharing,  g (g (g (g ...)))
    -- g (let x = g x in x)    -- two g stages, sharing

_Y 是一个非共享定点组合器,这里安排一个递归的“伸缩” 多阶段素数生产(生产者)。

这到底是什么意思?在这种情况下,什么是“共享”与“非共享”? _Yfix 有何不同?

【问题讨论】:

标签: haskell y-combinator letrec fixpoint-combinators


【解决方案1】:

“共享”是指f x 重新使用它创建的x;但是对于_Y g = g . g . g . g . ...,每个g 都会重新计算其输出(参见thisthis)。

在这种情况下,共享版本的内存使用率要低得多,leads to a space leak1

_Y 的定义反映了通常的 lambda 演算定义对 Y 组合子的影响,它模拟重复递归,而真正的递归指的是 相同的(因此,共享)实体。

    x      = f x
    (_Y g) = g (_Y g)

两个xs 都指代相同 实体,但每个(_Y g)s 指代等效但独立的实体。无论如何,这就是它的意图。

当然,由于引用透明性,Haskell 语言 不能保证任何这些。但是GHC 编译器 确实是这样的。

_Y g 是一个常见的子表达式,它可以被编译器“消除”,方法是给它一个名称并重用该命名实体,从而破坏它的整个目的。这就是为什么 GHC 有 “没有公共子表达式消除” -fno-cse 标志,它明确地阻止了这种情况。过去,您必须使用此标志才能在此处实现所需的行为,但现在不再如此。 GHC 将不再那么积极地消除常见的子表达式,使用更新的(阅读:现在几年)版本。

免责声明:我是您所指页面部分的作者。本来希望在 wiki 页面上经常出现这种来回,但它从来没有发生过,所以我的工作没有得到这样的审查。要么没人打扰,要么可以通过的(没有重大错误)。 Wiki 似乎已被废弃多年。


1所涉及的g函数,

(3:) . minus [5,7..] . foldr (\ (x:xs) ⟶ (x:) . union xs) [] 
                      . map (\ p ⟶ [p², p² + 2p..])

给定所有奇数素数的增加流,产生所有奇数素数的增加流。为了产生一个素数 N 的值,它至少消耗它的输入流直到高于 sqrt(N) 的第一个素数。因此,生产点大致通过重复平方给出,并且在这些链(或“塔”)中总共有 ~ log (log N) 个这样的g 函数素数生产者,每个都可以立即进行垃圾回收,最低的生产者只给出第一个奇数素数,3,这是先验已知的。

对于两阶段的_Y2 g = g x where { x = g x },链中只有两个,但只有最上面的一个会立即被垃圾回收,如上面引用的链接所述。

【讨论】:

    【解决方案2】:

    _Y 被翻译成以下STG:

    _Y f = let x = _Y f in f x
    

    fix 与 Haskell 源的翻译相同:

    fix f = let x = f x in x
    

    所以fix f 设置了一个递归thunk x 并返回它,而_Y 是一个递归函数,重要的是它不是尾递归的。强制_Y f 进入f,将new 调用作为参数传递给_Y f,因此每个递归调用都会设置一个new thunk;强制fix f返回的x进入f,将x本身作为参数传递,所以每个递归调用都进入同一个thunk——这就是“共享”的意思。

    共享版本通常有更好的内存使用,也让 GHC RTS 检测到一些无限循环。当一个 thunk 被强制时,在评估开始之前,它被一个“黑洞”取代;如果在评估 thunk 期间的任何时候从同一个线程到达黑洞,那么我们知道我们有一个无限循环并且可以抛出异常(您可能已经看到显示为 Exception: <<loop>>)。

    【讨论】:

    • 在这种情况下,共享版本更糟糕,导致空间泄漏。 :)(更多讨论和链接在我的回答中)。我们不想想要分享的另一种情况是,例如幂集计算。
    • @WillNess:你是对的;我在“更好的内存使用”声明中添加了“通常”限定词——它确实取决于你用它做什么。在改善共享和避免不必要的保留之间通常需要权衡取舍。
    • 是的。我想这取决于输出的重用部分有多大。以汉明数为例,对于 O(n) 序列,保留部分是 O(n^(2/3)),所以没关系。但是对于素数,保留的部分从 ~sqrt(n) 到 ~(n),所以它太大了。
    • 对于相反的示例,带有 sharing 修复的calculating fibonaccis 效率更高。
    【解决方案3】:

    从 GHC/Haskell 的角度来看,我认为您已经收到了很好的答案。我只是想插话并添加一些历史/理论注释。

    Hasegawa 的博士论文中对递归的展开视图和循环视图之间的对应关系进行了严格研究:https://www.springer.com/us/book/9781447112211

    (这是一篇简短的论文,无需支付 Springer 费用即可阅读:https://link.springer.com/content/pdf/10.1007%2F3-540-62688-3_37.pdf

    Hasegawa 假设了一个追踪的幺半群类别,这一要求远没有域理论中通常的 PCPO 假设那么严格,这构成了我们总体上如何看待 Haskell 的基础。 Hasegawa 展示的是,人们可以在这样的设置中定义这些“共享”不动点算子,并确定它们对应于 Church 的 lambda 演算中固定点的通常展开视图。也就是说,没有办法通过让它们产生不同的答案来区分它们。

    长谷川的对应关系适用于所谓的中心箭头;即,当不涉及“效果”时。后来,Benton 和 Hyland 扩展了这项工作,并表明当底层箭头也可以执行“温和”单子效应时,对应关系成立:https://pdfs.semanticscholar.org/7b5c/8ed42a65dbd37355088df9dde122efc9653d.pdf

    不幸的是,Benton 和 Hyland 只允许非常“温和”的效果:像 state 和 environment monad 这样的效果符合要求,但不适用于异常、列表或 IO 等一般效果。 (这些有效计算的定点运算符在 Haskell 中称为 mfix,类型签名为 (a -> m a) -> m a,它们构成了递归-do 表示法的基础。)

    如何扩展这项工作以涵盖任意一元效应仍然是一个悬而未决的问题。虽然这几天似乎没有受到太多关注。 (对于那些对 lambda 演算、一元效应和基于图形的计算之间的对应关系感兴趣的人来说,这将是一个很好的博士主题。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-12-27
      • 1970-01-01
      • 2013-02-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多