В 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
.