Haskell: Списки, Массивы, Векторы, Последовательности

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

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

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

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

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

1 ответов


Списки Рок

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

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

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

ones :: [Integer]
ones = 1:ones

twos = map (+1) ones

tenTwos = take 10 twos

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

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

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

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

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

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

Последовательности Функциональных

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

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

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

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

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

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

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

у вас есть другие варианты

если вы просто хотите списки с возможностью эффективной вставки в конце, вы можете использовать список разница. Лучший пример списков, искажающих производительность, как правило, исходит из [Char] которую прелюдия назвала String. Char списки convient, но, как правило, работают на порядок в 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 через любую вещь "list like".

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


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.