Haskell: Списки, Массивы, Векторы, Последовательности
Я изучаю Haskell и прочитал пару статей о различиях в производительности списков Haskell и (вставьте свой язык) массивов.
будучи учеником, я, очевидно, просто использую списки, даже не думая о разнице в производительности. Недавно я начал исследовать и нашел множество библиотек структуры данных, доступных в Haskell.
может кто - нибудь объяснить разницу между списками, массивами, векторами, последовательностями, не углубляясь в компьютер наука теория структур данных?
кроме того, существуют ли некоторые общие шаблоны, в которых вы бы использовали одну структуру данных вместо другой?
есть ли другие формы структур данных, которые мне не хватает и могут быть полезны?
1 ответ:
Списки Рок
безусловно, самой удобной структурой данных для последовательных данных в Haskell является List
data [a] = a:[a] | []
списки дают вам cons (1) минусы и соответствие шаблону. Стандартная библиотека, и в этом отношении прелюдия, полна полезных функций списка, которые должны засорять ваш код (
foldr
,map
,filter
). Списки - это постоянные, он же чисто функциональный, что очень приятно. Списки Haskell на самом деле не являются" списками", потому что они являются коиндуктивными (другие языки называют эти потоки) так что вещи, какones :: [Integer] ones = 1:ones twos = map (+1) ones tenTwos = take 10 twos
чудесно работать. Бесконечные структуры данных рок.
списки в Haskell предоставляют интерфейс, похожий на итераторы в императивных языках (из-за лени). Таким образом, имеет смысл, что они широко используются.
с другой стороны
первая проблема со списками заключается в том, что индексировать в них
(!!)
занимает Θ (k) время, что раздражает. Кроме того, добавление может быть медленным++
, но ленивая модель оценки Хаскелла означает, что они могут рассматриваться как полностью амортизированные, если они вообще происходят.вторая проблема со списками заключается в том, что они имеют плохую локальность данных. Реальные процессоры имеют высокие константы, когда объекты в памяти не расположены рядом друг с другом. Так, в C++
std::vector
имеет более быстрый "snoc" (размещение объектов в конце), чем любая чистая структура данных связанного списка, о которой я знаю, хотя это не постоянная структура данных, поэтому менее дружелюбная, чем Haskell списки.третья проблема со списками заключается в том, что они имеют низкую эффективность пространства. Пучки дополнительных указателей подталкивают ваше хранилище (с постоянным коэффициентом).
Последовательности Функциональных
Data.Sequence
внутренне основан на деревья палец (я знаю, вы не хотите этого знать), что означает, что у них есть некоторые хорошие свойства
- чисто функциональной.
Data.Sequence
это полностью устойчивые данные структура.- чертовски быстрый доступ к началу и концу дерева. Θ (1) (амортизируется) для получения первого или последнего элемента или для добавления деревьев. В списках вещей быстрее всего,
Data.Sequence
на постоянное медленнее.- Θ (log n) доступ к середине последовательности. Это включает в себя вставку значений для создания новых последовательностей
- высокое качество API
С другой стороны,
Data.Sequence
не делает много для проблемы локализации данных, и работает только для конечных коллекций (это менее лениво, чем списки)массивы не для слабонервных
массивы являются одной из самых важных структур данных в CS, но они не очень хорошо вписываются в ленивый чистый функциональный мир. Массивы обеспечивают Θ (1) доступ к середине сбора и исключительно хорошие данные о местоположении/постоянных факторах. Но, поскольку они не очень хорошо вписываются в Haskell, они являются болью для использования. Есть на самом деле множество различных типы массивов в текущей стандартной библиотеке. К ним относятся полностью устойчивые массивы, изменяемые массивы для монады ввода-вывода, изменяемые массивы для монады ST и неупакованные версии выше. Для получения дополнительной информации проверьте Haskell wiki
вектор-это" лучший " массив
The
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
vsData.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
.