haskell中循环列表和无限列表之间有什么区别?

时间:2014-08-26 05:07:03

标签: haskell infinite circular-list

引用@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小区,垃圾收集器必须在分配之前保留所有内容?

3 个答案:

答案 0 :(得分:17)

差异完全在于记忆表示。从语言的语义的角度来看,它们是无法区分的 - 你不能编写一个可以区分它们的函数,所以你的两个版本的cycle被认为是两个版本的相同的功能(它们与参数的结果完全相同)。事实上,我不知道语言定义是否保证其中一个是周期性的而另一个是无限的。

但无论如何,让我们带出ASCII艺术。周期清单:

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

无限名单:

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

循环列表中的内容是,从列表中的每个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将以thunk本身作为其参数调用f的目标代码,并用结果替换thunk的内容。

答案 1 :(得分:9)

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

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

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

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

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

答案 2 :(得分:1)

cycle xs = xs ++ cycle xs            -- 1
cycle xs = xs' where xs' = xs ++ xs' -- 2
  

这两种实现之间究竟有什么区别?

使用GHC,区别在于实现#2创建了自引用值(xs'),而#1只创建了一个恰好相同的thunk。

  

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

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


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