Списки рок
На сьогоднішній день найбільш дружньою структурою даних для послідовних даних у Haskell є Список
data [a] = a:[a] | []
Списки дають ϴ (1) мінуси та відповідність шаблонів. Стандартна бібліотека, а для цього важливо , прелюдія, сповнена корисних функцій списку , які повинні послід ваш код ( foldr
, map
, filter
). Списки є стійкими , вони є суто функціональними, що дуже приємно. Списки Haskell насправді не "списки", тому що вони є спільними (інші мови називають ці потоки), тому подібні речі
ones :: [Integer]
ones = 1:ones
twos = map (+1) ones
tenTwos = take 10 twos
працювати чудово. Нескінченні структури даних гойдаються.
Списки в Haskell надають інтерфейс, подібний до ітераторів на імперативних мовах (через лінь). Отже, має сенс, що вони широко використовуються.
З іншої сторони
Перша проблема зі списками полягає в тому, що для їх індексації (!!)
потрібно ϴ (k) час, що дратує. Також придатки можуть бути повільними++
, але лінива модель оцінки Хаскелла означає, що їх можна розглядати як повністю амортизовані, якщо вони взагалі трапляються.
Друга проблема зі списками полягає в тому, що вони мають погану локальність даних. Реальні процесори мають високі константи, коли об'єкти в пам’яті не розташовуються поруч. Так, у С ++std::vector
є швидший "snoc" (розміщення об'єктів у кінці), ніж будь-яка чиста структура пов'язаних списків, про яку я знаю, хоча це не стійка структура даних, настільки менш дружна, як списки Haskell.
Третя проблема зі списками полягає в тому, що вони мають низьку ефективність використання простору. Пучки додаткових покажчиків збільшують ваше сховище (постійним фактором).
Послідовності функціональні
Data.Sequence
основана на пальцевих деревах (я знаю, ви цього не хочете знати), це означає, що вони мають деякі приємні властивості
- Чисто функціональний.
Data.Sequence
є повністю стійкою структурою даних.
- Швидкий доступ до початку та кінця дерева. ϴ (1) (амортизовано), щоб отримати перший або останній елемент, або додати дерева. У речах списки найшвидші,
Data.Sequence
максимум постійні повільніше.
- ϴ (log n) доступ до середини послідовності. Сюди входить вставка значень для створення нових послідовностей
- Висока якість API
З іншої сторони, Data.Sequence
це не робить багато для проблеми локалізації даних, а працює лише для обмежених колекцій (це менш ліниво, ніж списки)
Масиви не для слабкого серця
Масиви є однією з найважливіших структур даних у CS, але вони не дуже добре вписуються у лінивий чистий функціональний світ. Масиви забезпечують ϴ (1) доступ до середини колекції та надзвичайно гарну локальність даних / постійні фактори. Але, оскільки вони не дуже добре вписуються в Haskell, вони є біль для використання. У поточній стандартній бібліотеці насправді існує безліч різних типів масивів. До них відносяться повністю стійкі масиви, змінні масиви для монади вводу-виводу, змінні масиви для монади СТ та недіагностичні версії вищевказаних. Для більш детальної перевірки вікі haskell
Вектор - це "кращий" масив
У 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
проти 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, kn)), де n - довжина послідовності, а k - індекс, який ви змінюєте.
Ви не можете легко зробити це за допомогою Vectors
та Arrays
. Вони можуть бути змінені, але це реальна імперативна модифікація, і це не можна робити в звичайному коді Haskell. Це означає, що операції в Vector
пакеті, які вносять модифікації на зразок snoc
і cons
повинні копіювати весь вектор, так що потребують O(n)
часу. Єдиним винятком з цього є те, що ви можете використовувати мутаційну версію ( Vector.Mutable
) всередині ST
монади (або IO
) і робити всі свої модифікації так, як ви хотіли б в обов'язковому порядку. Коли ви закінчите, ви "заморожуєте" свій вектор, щоб перетворитись на незмінну структуру, яку ви хочете використовувати з чистим кодом.
Моє відчуття, що ви повинні використовувати за замовчуванням, Data.Sequence
якщо список не відповідає. Використовуйте Data.Vector
лише в тому випадку, якщо ваша схема використання не передбачає внесення багатьох модифікацій або якщо вам потрібні надзвичайно високі показники роботи в монадах ST / IO.
Якщо всі ці розмови про ST
монаду залишають вас розгубленими: тим більше причин дотримуватися чистого швидкого і красивого Data.Sequence
.