В Haskell, почему нет TypeClass для вещей, которые могут действовать как списки?

Я читаю Learn You the Haskell, и мне интересно, почему так много вещей действует как список, и ничего в Prelude использует родное средство типов классов, чтобы установить это:

". Байт-версия: называется cons. Она берет байты и байты и ставит байт в начале. Но он ленив, поэтому он сделает новый кусок, даже если первый фрагмент в байтовой строке не будет заполнен, Поэтому лучше использовать строгую версию cons, cons," если вы собираетесь вставлять много байтов в начале байтовой строки".

Почему нет списка TypeClass или что-то, что предлагает функцию : для объединения Data.ByteString, Data.List, Data.ByteString.Lazy и т.д.? Есть ли причина для этого, или это просто элемент наследия Haskell? Использование : в качестве примера является своего рода преуменьшением, также из LYAH:

В противном случае модули bytestring имеют нагрузку функций, которые аналогичны функциям в Data.List, включая, но не ограничиваясь: head, tail, init, null, length, map, reverse, foldl, foldr, concat, takeWhile, фильтр и т.д.

Ответ 1

Пакет ListLike, похоже, обеспечивает то, что вы ищете. Я никогда не понимал, почему он не более популярен.

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

  • Контейнеры, которые вообще не заботятся о своих элементах (например, [])
  • Контейнеры, которые реализованы только для определенных элементов (например, bytestrings)
  • Контейнеры, которые являются полиморфными над элементами, но требуют контекста (например, Data.Vector.Storable, который будет иметь любой тип с возможностью хранения экземпляр).

Вот очень простой класс в стиле ListLike без использования каких-либо расширений:

class Listable container where
  head :: container a -> a

instance Listable [] where
  head (x:xs) = x

instance Listable ByteString where --compiler error, wrong kind

instance Listable SV.Vector where
  head v = SV.head    --compiler error, can't deduce context (Storable a)

Здесь container имеет вид *->*. Это не будет работать для байтов, потому что они не допускают произвольный тип; они имеют вид *. Он также не будет работать для вектора Data.Vector.Storable, потому что класс не включает контекст (ограничение Storable).

Вы можете решить эту проблему, изменив определение класса на

class ListableMPTC container elem | container -> elem where

или

class ListableAT container where
  type Elem container :: *

Теперь container имеет вид *; это полностью прикладной конструктор типов. То есть ваши экземпляры выглядят как

instance ListableMPTC [a] a where

но вы больше не Haskell98.

Поэтому даже простой интерфейс с интерфейсом Listable-type является нетривиальным; это становится немного сложнее, если у вас есть другая семантика коллекции для учета (например, очередей). Другая действительно большая проблема - данные с изменяемыми и неизменными. До сих пор каждая попытка, которую я видел (кроме одного), пыталась воспроизвести эту проблему, создав изменяемый интерфейс и неизменяемый. Один интерфейс, который я знаю, который объединил два, был изгиб ума, вызвал кучу расширений и имел довольно низкую производительность.

Добавление: bytestrings

Полностью догадка с моей стороны, но я думаю, что мы застряли с bytestrings как продукт эволюции. То есть они были первым решением для операций ввода-вывода с низкой производительностью, и имело смысл использовать Ptr Word8 для взаимодействия с системными вызовами IO. Операции над указателями требуют Storable, и, скорее всего, необходимые расширения (как описано выше) для выполнения работы по полиморфизму были недоступны. Теперь трудно преодолеть свой импульс. Подобный контейнер с полиморфизмом, безусловно, возможен, пакет storablevector реализует это, но он не так популярен.

Могут ли ошибки быть полиморфными без каких-либо ограничений на элементы? Я думаю, что ближайший Haskell к этому относится к типу Array. Это не так хорошо, как bytestring для низкоуровневого ввода-вывода, поскольку данные должны быть распакованы из указателя в внутренний формат массива. Кроме того, данные вставляются в бокс, что добавляет значительные накладные расходы. Если вы хотите распаковать содержимое (меньше места) и эффективно взаимодействовать с C, указатели - это путь. После того, как у вас есть Ptr, вам потребуется Storable, а затем вам нужно включить тип элемента в класс типа, так что вам останутся требующие расширения.

Считая, что с соответствующими расширениями это по существу является решаемой проблемой для любой реализации одного контейнера (modulo mutable/immutable API). Более сложная часть теперь включает разумный набор классов, которые могут использоваться для различных типов структур (списки, массивы, очереди и т.д.) И достаточно гибки, чтобы быть полезными. Я лично ожидал бы, что это будет относительно просто, но я могу ошибаться.

Ответ 2

который предлагает: функцию унификации Data.ByteString, Data.List, Data.ByteString.Lazy и т.д.

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

Существует несколько проектов для унификации этих разных типов данных, связанных с полуопределением, с одним интерфейсом, поэтому я надеюсь, что мы скоро увидим результат. Аналогично для типов контейнеров. Результат не будет тривиальным, хотя.

Ответ 3

Основная проблема с таким классом состоит в том, что даже если бы он существовал, он бы предлагал только поверхностное сходство.

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

В случае строгих байтов, создающих их с минусами, ужасно, потому что вы завершаете копирование всей строки каждый раз, когда вы добавляете еще один Char. Эта операция O (1) в списке превращает ее в операцию O (n) в Bytestring.

Это приводит к поведению O (n ^ 2), когда вы реализуете первый алгоритм, который может прийти на ум, карта, тогда как создание списка или Data.Sequence.Seq с cons - это линейное время, и оно может быть реализовано в O (n) для байтов или векторов, а также с небольшим количеством мыслей.

Оказывается, полезность такого класса в свете этого более поверхностна, чем фактическая.

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

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

tl; dr Обычно вы хотите реализовать алгоритмы очень по-разному, когда изменяется асимптотика базовых операций.

Ответ 4

Существуют два типа классов, называемых Foldable и Traversable, которые направлены на абстрактное поведение отдельных типов списков и других последовательных структур данных. Однако не все структуры данных имеют экземпляры этих данных, и я не знаю, достаточно ли они достаточно прозрачны для компилятора, чтобы он мог выполнять оптимизацию на них (кто-нибудь знает об этом?)

Источник: Складные и перемещаемые
См. Также этот ответ Почему Haskell отсутствует "очевидный" Typeclasses

Ответ 5

ByteString не является общим типом.

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

class Seq a b | a -> b where
  head :: a -> b
  isTail :: a -> Bool

# ([a]) is a sequence of a's
instance Seq [a] a where
  head (x:xs) = x
  isTail = (== [])

# ByteString is a sequence of chars
instance Seq ByteString Char

Или попробуйте это?

type BS a = ByteString
instance List BS

Ответ 6

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

Существует значение для класса типа, которое предоставляет общую функцию toList, но уже существующую в Data.Foldable.

В основном, решение состоит в реализации Data.Foldable и использовании его функции toList.