Haskell: списки, массивы, векторы, последовательности

Я изучаю Haskell и читаю несколько статей о различиях в производительности списков Haskell и (вставляем свой язык).

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

Может кто-нибудь объяснить разницу между списками, массивами, векторами, последовательностями, не углубляясь в теорию информационных систем данных?

Кроме того, существуют ли некоторые общие шаблоны, в которых вы бы использовали одну структуру данных вместо другой?

Существуют ли какие-либо другие формы структур данных, которые мне не хватает и могут быть полезны?

Ответ 1

Список Rock

На сегодняшний день наиболее дружественной структурой данных для последовательных данных в Haskell является List

 data [a] = a:[a] | []

Списки дают вам (1) минусы и соответствие шаблону. Стандартная библиотека, и в этом отношении прелюдия, полна полезных функций списка, которые должны засорять ваш код (foldr, map, filter). Списки являются постоянными, ака чисто функциональными, что очень приятно. Списки Haskell на самом деле не являются "списками", потому что они являются коиндуктивными (другие языки называют эти потоки), поэтому такие вещи, как

ones :: [Integer]
ones = 1:ones

twos = map (+1) ones

tenTwos = take 10 twos

работают чудесно. Бесконечные структуры данных рок.

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

С другой стороны

Первая проблема со списками состоит в том, что для индексации в них (!!) выполняется время Θ (k), что раздражает. Кроме того, добавление может быть медленным ++, но Haskell ленивая модель оценки означает, что они могут рассматриваться как полностью амортизированные, если они вообще происходят.

Вторая проблема со списками заключается в том, что они имеют плохую локальность данных. Реальные процессоры несут высокие константы, когда объекты в памяти не располагаются рядом друг с другом. Таким образом, в С++ std::vector имеет более быстрый "snoc" (помещая объекты в конец), чем любая структура данных с чистым связанным списком, о которой я знаю, хотя это не является устойчивой структурой данных, менее менее дружественной, чем списки Haskell.

Третья проблема со списками состоит в том, что они имеют низкую эффективность использования. Пучки дополнительных указателей подталкивают ваше хранилище (постоянным фактором).

Последовательности являются функциональными

Data.Sequence внутренне основан на деревьях пальцев (я знаю, вы не хотите знать это), что означает, что у них есть некоторые приятные свойства

  • Чисто функциональный. Data.Sequence - полностью устойчивая структура данных.
  • Быстрый доступ к началу и концу дерева. Θ (1) (амортизируется), чтобы получить первый или последний элемент или добавить деревья. В списках вещей быстрее всего, Data.Sequence не более медленнее.
  • Θ (log n) доступ к середине последовательности. Это включает в себя вставку значений для создания новых последовательностей.
  • API высокого качества

С другой стороны, Data.Sequence мало что делает для проблемы локальности данных и работает только для конечных коллекций (он менее ленив, чем списки)

Массивы не для слабонервных

Массивы являются одной из наиболее важных структур данных в CS, но они не очень хорошо сочетаются с ленивым чистым функциональным миром. Массивы обеспечивают Θ (1) доступ к середине коллекции и исключительно хорошую локальность данных/постоянные факторы. Но, поскольку они не очень хорошо вписываются в Haskell, они больно использовать. На самом деле в текущей стандартной библиотеке существует множество различных типов массивов. К ним относятся полностью устойчивые массивы, изменяемые массивы для монады IO, изменяемые массивы для монады ST и версии с короткими версиями выше. Для дополнительной проверки haskell wiki

Вектор - это "лучший" массив

Пакет Data.Vector обеспечивает все преимущества массива, в более высоком уровне и более чистом API. Если вы действительно не знаете, что делаете, вы должны использовать их, если вам нужен массив, такой как производительность. Конечно, некоторые оговорки все еще применяются - изменяемый массив, такой как структуры данных, просто не играет на чистых ленивых языках. Тем не менее, иногда вы хотите, чтобы производительность O (1) и Data.Vector предоставлялась вам в полезном пакете.

У вас есть другие опции

Если вам просто нужны списки с возможностью эффективной вставки в конце, вы можете использовать список различий . Наилучший пример списков, закручивающих производительность, как правило, исходит от [Char], который прелюдия имеет псевдоним как String. Char списки удобны, но имеют тенденцию работать примерно в 20 раз медленнее, чем строки C, поэтому не стесняйтесь использовать Data.Text или очень быстрый Data.ByteString. Я уверен, что есть другие библиотеки, ориентированные на последовательность, о которых я сейчас не думаю.

Заключение

90 +% времени, когда мне нужна последовательная коллекция в списках Haskell, являются правильной структурой данных. Списки похожи на итераторы, функции, которые потребляют списки, могут быть легко использованы с любой из этих других структур данных, используя функции toList, с которыми они связаны. В лучшем мире прелюдия будет полностью параметрической в ​​отношении того, какой тип контейнера она использует, но в настоящее время [] помещает стандартную библиотеку. Таким образом, используя списки (почти), где все определенно хорошо. Вы можете получить полностью параметрические версии большинства функций списка (и благородно их использовать)

Prelude.map                --->  Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc    --->  Data.Foldable.foldr/foldl/etc
Prelude.sequence           --->  Data.Traversable.sequence
etc

Фактически, Data.Traversable определяет API, который более или менее универсален для любой "списка".

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


EDIT: на основе комментариев я понимаю, что никогда не объяснял, когда использовать Data.Vector vs Data.Sequence. Массивы и векторы обеспечивают чрезвычайно быструю операцию индексирования и нарезки, но являются принципиально временными (императивными) структурами данных. Чистые функциональные структуры данных, такие как Data.Sequence и [], позволяют эффективно создавать новые значения из старых значений, как если бы вы модифицировали старые значения.

  newList oldList = 7 : drop 5 oldList

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

  newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence 

создаст новую последовательность с newValue вместо ее элемента 3000. Опять же, это не разрушает старую последовательность, она просто создает новую. Но он делает это очень эффективно, принимая O (log (min (k, k-n)), где n - длина последовательности, а k - индекс, который вы изменяете.

Вы легко можете сделать это с помощью Vectors и Arrays. Они могут быть изменены, но это настоящая императивная модификация, и поэтому это невозможно сделать в обычном коде Haskell. Это означает, что операции в пакете Vector, которые делают такие модификации, как snoc и cons, должны копировать весь вектор, поэтому возьмите O(n) время. Единственным исключением является то, что вы можете использовать изменяемую версию (Vector.Mutable) внутри монады ST (или IO) и выполнять все ваши изменения так же, как и на императивном языке. Когда вы закончите, вы "заморозите" свой вектор, чтобы включить в неизменную структуру, которую хотите использовать с чистым кодом.

Я чувствую, что вы должны по умолчанию использовать Data.Sequence, если список не подходит. Используйте Data.Vector только в том случае, если ваш шаблон использования не предусматривает внесения многих изменений или если вам нужна чрезвычайно высокая производительность в монадах ST/IO.

Если все эти разговоры о монаде ST оставляют вас в замешательстве: тем более разумно придерживаться чистой быстрой и красивой Data.Sequence.