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

Ссылаясь на ответ @dfeuer на этот вопрос: Наименее дорогой способ построения циклического list в Haskell, в котором говорится, что использование циклических списков «побеждает» сборщик мусора, поскольку он должен сохранять все, что вы использовали из циклического списка, выделенным до тех пор, пока вы не удалите ссылку на любые cons-ячейки в списке.

По-видимому, в Haskell циклический список и бесконечный список — это две разные вещи. Этот блог (https://unspecified.wordpress.com/2010/03/30/a-double-linked-list-in-haskell/) говорит, что если вы реализуете cycle следующим образом:

cycle xs = xs ++ cycle xs

это бесконечный список, а не циклический. Чтобы сделать его циклическим, вы должны реализовать его следующим образом (как можно найти в исходном коде Prelude):

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

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


person Ramith Jayatilleka    schedule 26.08.2014    source источник
comment
Точно так же, как и в других языках...   -  person leppie    schedule 26.08.2014
comment
Я неправильно прочитал и ожидал плохой шутки о людях, которые ездят на велосипедах...   -  person Rob P.    schedule 26.08.2014
comment
Связанный вопрос, похоже, относился к очень длинным циклам. cycle обычно дает очень хорошие результаты для коротких циклов, но для длинных, то, как вы используете эту вещь, будет определять, поможет ли представление бесконечного списка или вам лучше изменить структуру ваша программа.   -  person dfeuer    schedule 27.08.2014


Ответы (3)


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

person Luis Casillas    schedule 26.08.2014
comment
может fix' называться _Y возможно? потому что это, в конце концов, комбинатор Y, эмулирующий совместное использование с копированием данных. - person Will Ness; 27.08.2014
comment
@WillNess, это зависит от того, как именно вы определяете термин «комбинатор Y». Насколько я понимаю, в самом узком смысле он относится к термину лямбда λf.(λx.f (x x)) (λx.f (x x)), а комбинатор с фиксированной точкой относится к функции, которую этот термин обозначает. Но, конечно, эти слова часто используются гораздо более свободно, чем это. - person Luis Casillas; 27.08.2014
comment
да, именно так, как тот лямбда-член, который вы показали. fixpoint является общим, комбинатор Y очень специфичен; моя точка зрения заключалась в том, что, как и в лямбда-исчислении, ссылка на себя (совместное использование) эмулируется посредством копирования терминов (данных) в Y (поскольку в лямбда-исчислении сокращение определяется через текстовую замену). - person Will Ness; 27.08.2014

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

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

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

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

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

person GS - Apologise to Monica    schedule 26.08.2014
comment
с другой стороны, с циклическим списком вся длина префикса будет храниться в памяти, тогда как с бесконечным будет храниться только длина используемой границы, которая может составлять всего 1 элемент. Я думаю, это некоторые случаи, которые вы упоминаете в конце своего ответа. :) - person Will Ness; 27.08.2014
comment
это я и имел в виду, в некоторых случаях циклический вариант будет хуже - person GS - Apologise to Monica; 27.08.2014
comment
да, только что закончил читать. :) вы также можете явно упомянуть оптимизацию cse и флаг -no-cse. - person Will Ness; 27.08.2014
comment
Существует семантическая разница в том, что циклический список повторяет ряд элементов снова и снова, в то время как бесконечный список может этого не делать. Не всякий бесконечный список можно превратить в циклический. - person sth; 27.08.2014
comment
Сможет ли CSE GHC сработать в этой ситуации? - person GS - Apologise to Monica; 27.08.2014
comment
@sth IIUYC, вы ссылаетесь только на заголовок, но в контексте текста вопроса ясно, я думаю, что ОП имел в виду повторяющийся список, полученный либо путем фактической манипуляции с указателем, либо путем копирования на элементы на неопределенный срок. Конечно, [1..] или cycle [1..] на самом деле не являются циклическими, или вы имели в виду что-то другое? - person Will Ness; 27.08.2014
comment
@WillNess разве лень все еще не применима к циклическим спискам? Если я сделаю xs = [1..10000000] ++ xs, это не займет сразу всю память. - person PyRulez; 05.05.2016
comment
@PyRulez, конечно. все еще остается вопрос, сохраняется ли структура списка в памяти, созданной [1..1000000], или нет, поскольку само определение бездействует, пока не оживет по требованию какого-то print upstream. Haskell list==Генератор Python + хранилище результатов IF необходимо. [1..10000000] — это определение генератора, которое можно просто запустить во второй раз вместо сохранения всех ранее сгенерированных элементов. делать дополнительные усилия по повторной генерации намного лучше, чем занимать огромные области памяти (но не маленькие, там по-другому). - person Will Ness; 05.05.2016

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

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

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

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

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


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

person Dan Burton    schedule 04.09.2014