Почему в Haskell отсутствуют "очевидные" классы типов

Рассмотрим объектно-ориентированные языки:

Большинство людей, имеющих опыт объектно-ориентированного программирования, знакомы с общими и интуитивно понятными интерфейсами на разных языках, которые отражают суть Java. Collection & List интерфейсы. Collection относится к совокупности объектов, которые не обязательно имеют естественный порядок / индексацию. List это коллекция, которая имеет естественный порядок / индексацию. Эти интерфейсы абстрагируют многие библиотечные структуры данных в Java, равно как и их эквивалентные интерфейсы в других языках, и для эффективной работы с большинством библиотечных структур данных требуется глубокое понимание этих интерфейсов.

Переход на Хаскелл:

Haskell имеет систему классов типов, которая действует на типы аналогично интерфейсам на объектах. У Haskell, кажется, есть хорошо разработанная иерархия классов типов в отношении Функторов, Применимых, Монад и т. Д., Когда тип рассматривает функциональность. Очевидно, они хотят правильных и хорошо абстрагированных классов типов. Тем не менее, когда вы смотрите на многие контейнеры Haskell (List, Map, Sequence, Set, Vector они почти все имеют очень похожие (или идентичные) функции, но не абстрагируются через классы типов.

Некоторые примеры:

  • null для проверки "пустоты"
  • length / size для количества элементов
  • elem / member для включения набора
  • empty и / или singleton для строительства по умолчанию
  • union для набора союза
  • (\\) / diff для заданной разницы
  • (!) / (!!) для небезопасной индексации (частичная функция)
  • (!?) / lookup для безопасного индексирования (общая функция)

Если я хочу использовать любую из перечисленных выше функций, но я импортировал два или более контейнеров, я должен начать скрывать функции от импортированных модулей или явно импортировать только необходимые функции из модулей или квалифицировать импортированные модули. Но поскольку все функции обеспечивают одинаковую логическую функциональность, это просто кажется хлопотным. Если функции были определены из классов типов, а не отдельно в каждом модуле, механика вывода типов компилятора могла бы решить эту проблему. Это также сделало бы переключение базовых контейнеров простым, если они разделяют классы типов (то есть: давайте просто использовать Sequence вместо List для лучшей эффективности произвольного доступа).

Почему у Хаскелла нет Collection и / или Indexable Тип-класс (ы), чтобы объединить и обобщить некоторые из этих функций?

7 ответов

Решение

Частично, причина в том, что монады и стрелы являются новыми, инновационными функциями Haskell, в то время как коллекции являются относительно более приземленными. Haskell имеет долгую историю как язык исследований; Интересные вопросы исследования (проектирование экземпляров монад и определение общих операций для монад) требуют больше усилий по разработке, чем полировка "промышленного уровня" (определение API-интерфейсов контейнеров).

Частично, причина в том, что эти типы происходят из трех разных пакетов (база, контейнеры и вектор), с тремя отдельными историями и дизайнерами. Это мешает их дизайнерам координировать предоставление экземпляров любого класса одного типа.

Частично, причина в том, что определить класс одного типа для всех пяти упомянутых вами контейнеров действительно сложно. List, Sequence и Vector относительно похожи, но Map и Set имеют совершенно разные ограничения. Для List, Sequence и Vector вам нужен простой класс конструктора, но для Set он не будет работать, поскольку Set требует экземпляра Ord для типа элемента. Хуже того, Map может поддерживать большинство ваших методов, но для ее одноэлементной функции нужны два параметра, а для остальных - только один.

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

В языке, подобном Java, функции не являются "гражданами первого класса"; Это правда, что анонимные функции доступны в последних версиях, но этот стиль интерфейса (Collection, Indexable, Interable и т. д.) был разработан до этого.

Это приводит к утомительной передаче нашего кода, поэтому мы предпочитаем , чтобы данные других людей передавались в наш код. Например:

  • Данные, реализующие Java Iterable давайте напишем for (Foo x : anIterable) { ... }
  • Данные, реализующие PHP ArrayAccess давайте напишем anArrayAccess[anIndex]

Этот стиль также можно увидеть в ОО-языках, которые реализуют генераторы, так как это еще один способ для нас написать for yieldedElement in aGenerator: ...,

Haskell использует другой подход к своим классам типов: мы предпочитаем, чтобы наш код передавался другим людям. Несколько (упрощенных) примеров:

  • Functor Мы принимаем наш код и применяем его к любым элементам, которые они содержат
  • Monad Примите наш код и примените его в некоторой "последовательности"
  • Foldable Мы принимаем наш код и используем его для "сокращения" своего содержания.

Java нужна только Iterable так как мы должны вызвать наш код в нашем for цикл, поэтому мы можем убедиться, что он вызывается правильно. Для Haskell требуются более конкретные классы типов, поскольку чужой код будет вызывать наш, поэтому нам нужно указать, как он должен вызываться; это map, fold, unfold, так далее.?

К счастью, система типов помогает нам выбрать правильный метод;)

lens Пакет предоставляет некоторые из этого.

  • Тестирование на пустоту, создание пустых контейнеров. AsEmpty класс типов из Control.Lens.Empty,

  • Доступ к элементам по ключу / индексу. At а также Ixed классы от Control.Lens.At,

  • Проверка на членство в set-like контейнерах. Contains класс типов из Control.Lens.At,

  • Добавление и удаление элементов в контейнерах, похожих на последовательности. Cons а также Snoc классы от Control.Lens.Cons,

Так же pure метод Applicative Класс типов часто можно использовать для создания "синглтонных" контейнеров. Для вещей, которые не являются функторами / аппликативами в Haskell, например Set возможно point от Data.Pointed может быть использован.

В Haskell есть несколько классов типов для работы с коллекциями в базовом пакете: Functor, Foldable и Traversable могут быть полезны для работы с коллекциями, а классы типов Monoid, Applicative и / или Alternative могут быть полезны для конструирования коллекций.

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

null для проверки "пустоты"

Складные подставки null с базы 4.8 (any (const True) является альтернативой для более ранних версий).

длина / размер для количества элементов:

Складные подставки length с базы 4.8 (getSum . foldMap (const 1) является альтернативой для более ранних версий).

элемент / член для включения набора

Складные подставки elem, notElem а также member,

пусто и / или одиночно для конструкции по умолчанию

Для пустых есть mempty из моноида и empty из альтернативы. Для синглтона есть pure от Applicative.

союз для множества союз

Есть mappend из моноида и <|> из альтернативы. Они не обязательно реализуют объединение множеств, но они реализуют некоторую форму объединения, которая хорошо работает вместе с empty и обычно также с singleton и find.

(\) / diff для заданной разности

К сожалению, этот не поддерживается.

(!) / (!!) для небезопасной индексации (частичная функция)

Вы могли бы использовать fromJust вместе с функцией безопасной индексации.

(!?) / поиск безопасного индексирования (общая функция)

Есть find от складной

Такие классы типов существуют в стандартном Haskell, но они не имеют того же имени, что и их эквивалентные OO-аналоги. Collection Класс типов, например, называется Foldable в Хаскеле. Вы можете использовать его для проверки, если структура пуста (foldr (const False) True x) или подсчитать количество элементов (foldMap (const 1) x) или для проверки на членство в наборе (foldr (\e' present -> (e==e') || present) False x для некоторых e).

Для таких операций, как поиск элементов, у вас есть Array класс типов, который может работать для последовательных данных. Для большей гибкости вы можете написать свой собственный Indexable класс, как это, например, (остерегайтесь линз):

class Indexable m k a where
  at :: k -> Lens' m (Maybe a)

Нулевой элемент и множественное объединение принадлежат Monoid класс типов (где mappend == union). В этом свете разность множеств также может быть реализована в своем собственном классе типов Differentiable (который, я уверен, уже существует в десятках библиотек Haskell), и у нас была бы полная совместимость с императивными языками.

Haskell, благодаря тому, что его разработали математики и тому подобное, не использует тот же словарь, что и большинство других языков, но будьте уверены, это не значит, что это не практичный язык в дополнение к тому, что он классный:-)

Законы. Хороший класс типов имеет законы. Большой класс типов обладает достаточной параметричностью, поэтому его законы являются "бесплатными теоремами". Класс типов без законов - это просто временная перегрузка имени.

Кроме того, проверьте classy-prelude и Edison-API.

У вас есть классы типов для разных аспектов сбора:

  1. состав: моноид (модуль Data.Monoid)

  2. последовательное управление: Applicative, Monad (модули Control.Applicative, Control.Monad)

  3. последовательная композиция: Alternative, MonadPlus (модули Control.Applicative, Control.Monad)

  4. непоследовательное отображение и редукция: Functor (мод. Data.Functor), Foldable (мод. Data.Foldable)

  5. последовательное отображение и сокращение: Traversable (модуль Data.Traversable)

  6. сериализация: двоичная (мод. Data.Binary)

  7. сравнение: Eq, Ord (мод. Data.Eq, Data.Ord)

  8. Текстуализация: Показать, Читать

  9. глубокая оценка (в нормальной форме): NFData (мод. Control.DeepSeq)

  10. универсальная проходимость типов данных: данные (мод. данные. данные)

За исключением того, что мономорфные коллекции (ByteString, IntSet, Text) не могут реализовать Functor и Foldable (для них требуется тип arity == 1 (Kind: * -> *))

Также ни (Set a) не реализует Functor.

Пакет mono-traversable переопределяет некоторые классы без исключения мономорфных типов.

Обновление Существует попытка поместить большинство функций в классы типов с помощью пакетов mono-traversable и classy-prelude.

библиотека ссылка, платформа

Другие вопросы по тегам