haskell中的循环列表和无限列表有啥区别?

Posted

技术标签:

【中文标题】haskell中的循环列表和无限列表有啥区别?【英文标题】:What is the difference between a cyclic list and an infinite list in haskell?haskell中的循环列表和无限列表有什么区别? 【发布时间】:2014-10-19 08:13:26 【问题描述】:

引用@dfeuer 对这个问题的回答:Least expensive way to construct cyclic list in Haskell,它表示使用循环列表“击败”垃圾收集器,因为它必须保留您从分配的循环列表中消耗的所有内容,直到您放弃对任何缺点的引用列表中的单元格。

显然,在 Haskell 中,循环列表和无限列表是两个不同的东西。这个博客(https://unspecified.wordpress.com/2010/03/30/a-doubly-linked-list-in-haskell/)说如果你实现cycle如下:

cycle xs = xs ++ cycle xs

它是一个无限列表,而不是循环列表。要使其循环,您必须像这样实现它(在 Prelude 源代码中可以找到):

cycle xs = xs' where xs' = xs ++ xs'

这两种实现之间究竟有什么区别?为什么如果你在循环列表的某个地方持有一个 cons 单元,垃圾收集器必须在分配之前保留所有内容?

【问题讨论】:

和其他语言完全一样... 我误读并期待一个关于骑自行车的人的坏笑话...... 链接的问题似乎是指非常长周期。 cycle 通常会在 short 周期中提供非常好的结果,但对于长周期,您使用该事物的方式将决定无限列表表示是否可以解决问题,或者您是否最好更改程序的结构。 【参考方案1】:

区别完全在于内存表示。从语言语义的角度来看,它们是无法区分的——你不能编写一个可以区分它们的函数,所以你的两个版本 cycle 被认为是同一个函数的两个实现(它们是参数到结果的完全相同的映射)。事实上,我不知道语言定义是否保证其中一个是循环的,另一个是无限的。

但无论如何,让我们带出 ASCII 艺术。循环列表:

   +----+----+                 +----+----+
   | x0 |   ----->   ...   --->| xn |    |
   +----+----+                 +----+-|--+
     ^                                |
     |                                |
     +--------------------------------+

无限列表:

   +----+----+
   | x0 |   ----->  thunk that produces infinite list
   +----+----+

循环列表的问题是,从列表中的每个 cons 单元格都有一条通往所有其他 及其自身的路径。这意味着从垃圾收集器的角度来看,如果其中一个 cons 单元是可达的,那么所有的都是。另一方面,在普通的无限列表中,没有任何循环,因此从给定的 cons 单元格中只能到达其后继单元。

请注意,无限列表表示比循环表示更强大,因为循环表示仅适用于在一定数量的元素之后重复的列表。例如,所有素数的列表可以表示为无限列表,但不能表示为循环列表。

另请注意,这种区别可以概括为两种不同的方式来实现fix 函数:

fix, fix' :: (a -> a) -> a
fix  f = let result = f result in result
fix' f = f (fix' f)

-- Circular version of cycle:
cycle  xs = fix (xs++)

-- Infinite list version of cycle:
cycle' xs = fix' (xs++)

GHC 库适用于我的 fix 定义。 GHC 编译代码的方式意味着为result 创建的thunk 用作f 应用程序的结果和参数。即,thunk 强制执行时,将调用 f 的目标代码,并将 thunk 本身作为其参数,并用结果替换 thunk 的内容。

【讨论】:

可以将fix' 称为_Y 吗?因为这就是它的本质——Y 组合器,通过复制数据来模拟共享。 @WillNess 这取决于您如何准确定义“Y 组合器”一词。据我了解,在最狭义的意义上,它指的是 lambda 术语 λf.(λx.f (x x)) (λx.f (x x)),而“定点组合器”指的是该术语所表示的 函数。但当然,这些词的使用通常比这更宽松。 是的,就像您展示的那个 lambda 术语一样。 “定点”是通用的,“Y 组合器”是非常具体的;我的观点是,就像在 lambda 演算中一样,自我引用(“共享”)是通过在 Y 中复制术语(数据)模拟(因为在 lambda 演算中减少是通过文本替换来定义的)。【参考方案2】:

循环列表和无限列表在操作上是不同的,但在语义上是不同的。

循环列表实际上是内存中的一个循环 - 想象一个单链表,其指针跟随一个循环 - 所以占用恒定空间。因为列表中的每个单元格都可以从任何其他单元格到达,所以持有任何一个单元格都会导致整个列表被持有。

当您评估更多列表时,无限列表将占用越来越多的空间。如果不再需要较早的元素,则将对其进行垃圾收集,因此处理它的程序可能仍会在恒定空间中运行,尽管垃圾收集的开销会更高。如果需要列表中较早的元素,例如因为您持有对列表头部的引用,则列表将在您评估它时消耗线性空间,并最终耗尽可用内存。

造成这种差异的原因是,在没有优化的情况下,像 GHC 这样的典型 Haskell 实现将为一个 分配一次内存,例如 cycle 的第二个定义中的 xs',但会重复为函数调用分配内存,如第一个定义中的cycle xs

原则上优化可能会将一种定义转换为另一种定义,但由于性能特征完全不同,因此在实践中不太可能发生这种情况,因为编译器通常对让程序表现得更糟非常保守。在某些情况下,由于已经提到的垃圾收集属性,循环变体会更糟。

【讨论】:

另一方面,使用循环列表,前缀的整个长度将保存在内存中,而无限仅保存使用中的边界长度,可能短至 1 个元素.我想这是您在答案末尾提到的“某些情况”。 :) 这就是我所说的“在某些情况下循环变体会更糟”的意思 是的,刚读完。 :) 您还可以明确提及 cse 优化和 -no-cse 标志。 存在语义差异,循环列表一遍又一遍地重复多个元素,而无限列表可能不会这样做。不是每个无限列表都可以变成循环列表。 @sth IIUYC,您仅指标题,但在问题文本的上下文中,很明显我认为 OP 意味着 repeating 列表,或者通过实际的指针操作,或通过复制元素,无限期地。当然 [1..]cycle [1..] 实际上不是循环的,还是你的意思是别的?【参考方案3】:
cycle xs = xs ++ cycle xs            -- 1
cycle xs = xs' where xs' = xs ++ xs' -- 2

这两种实现之间到底有什么区别?

使用 GHC,不同之处在于实现 #2 创建一个自引用值 (xs'),而 #1 仅创建一个恰好相同的 thunk。

为什么如果你在一个循环列表的某个地方持有一个 cons 单元,那么垃圾收集器必须在分配之前保留所有内容?

这又是特定于 GHC 的。正如 Luis 所说,如果您引用了循环列表中的一个 cons 单元格,那么您只需绕过循环即可到达整个列表。垃圾收集器是保守的,不会收集你仍然可以到达的任何东西。


Haskell 是纯粹的,并且重构是合理的……只有当您忽略内存使用(以及其他一些事情,如 CPU 使用和计算时间)时。 Haskell 语言没有指定编译器应该做什么来区分#1 和#2。 GHC 的实现遵循某些合理的内存管理模式,但不是很明显。

【讨论】:

以上是关于haskell中的循环列表和无限列表有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个 Haskell 代码可以成功地处理无限列表?

Haskell中无限列表的执行部分?

无限列表的 Haskell 笛卡尔积

Haskell 中的列表是归纳的还是归纳的?

使用包含或循环列表之间有啥大区别吗?

Data Parallel Haskell 中的 PArray 和 [::] 有啥区别?