В чем разница между циклическим списком и бесконечным списком в haskell?

ссылка на ответ @dfeuer на этот вопрос:наименее дорогой способ построения циклического списка в 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'

в чем именно разница между этими двумя реализациями? И почему, если вы держитесь за одну ячейку минусов где-то в циклическом списке, то сборщик мусора должен держать все, прежде чем он выделен, а также?

3 ответов


разница полностью в представлении памяти. С точки зрения семантики языка они неразличимы-вы не можете написать функцию, которая может их различать, поэтому ваши две версии cycle рассматриваются две реализации одной и той же функции (они же сопоставление аргументов к результатам). На самом деле, я не знаю, гарантирует ли определение языка, что один из них цикличен, а другой бесконечен.

но в любом случае, давайте выведем искусство ASCII. Циклический список:

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

бесконечный список:

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

дело с циклическим списком в том, что из каждой ячейки минусов в списке есть путь ко всем остальным и сам. Это означает, что с точки зрения сборщика мусора, если одна из ячеек минусов достижима, то доступны все. В простом бесконечном списке, с другой стороны, нет никаких циклов, поэтому из данной ячейки минусов только ее преемники доступны.

обратите внимание, что представление бесконечного списка более мощное, чем циклическое, потому что циклическое представление работает только со списками, которые повторяются после некоторого количества элементов. Например, Список всех простых чисел может быть представлен как бесконечный список, но не как циклический.

Обратите также внимание, что это различие можно обобщить на два разных способа реализации 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 означает, что thunk создан для result используется и как результат и аргумент применения f. То есть, thunk при принудительном вызове вызовет объектный код для f С самим thunk в качестве аргумента и замените содержимое thunk результатом.


циклические списки и бесконечные списки различны операционно, но не семантически.

циклический список-это буквально цикл в памяти-представьте себе односвязный список с указателями после цикла-поэтому занимает постоянное пространство. Поскольку каждая ячейка в списке может быть доступна из любой другой ячейки, удержание одной ячейки приведет к удержанию всего списка.

бесконечный список будет занимать все больше и больше места по мере его оценки. Более ранние элементы будет сбор мусора, если он больше не нужен, поэтому программы, которые его обрабатывают, могут по-прежнему работать в постоянном пространстве, хотя накладные расходы на сборку мусора будут выше. Если более ранние элементы в списке необходимы, например, потому, что вы держите ссылку на начало списка, то список будет потреблять линейное пространство по мере его оценки и в конечном итоге исчерпает доступную память.

причина этой разницы заключается в том, что без оптимизации типичная реализация Haskell как GHC выделит память один раз для стоимостью, как xs' во втором определением cycle, но будет повторно выделять память для вызова функции, например cycle xs в первом определении.

в принципе оптимизация может превратить одно определение в другое, но из-за совершенно разных характеристик производительности маловероятно, что это произойдет на практике, поскольку компиляторы, как правило, довольно консервативны в отношении поведения программ хуже. В некоторых случаях циклический вариант будет хуже из-за уже упомянутых свойств сборки мусора.


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

в чем именно разница между этими двумя реализациями?

используя GHC, разница в том, что реализация #2 создает самореферентное значение (xs'), в то время как #1 просто создает thunk, который оказывается тем же самым.

и почему, если вы держитесь за одну ячейку минусов где-то в циклическом списке, сборщик мусора должен сохранить все, прежде чем он будет выделен как ну?

это снова специфический GHC. Как сказал Луис, если у вас есть ссылка на одну ячейку минусов в циклическом списке, то вы можете достичь всего списка, просто обойдя цикл. Сборщик мусора консервативен и не будет собирать ничего, что вы все еще можете достичь.


Haskell является чистым и где-рефакторинг звук... только когда вы игнорируете использование памяти (и некоторые другие вещи, такие как использование процессора и время вычислений). Хэскелл язык не укажите, что компилятор должен делать, чтобы различать #1 и #2. GHC реализация следует определенным шаблонам управления памятью, которые являются разумными, но не сразу очевидными.