Подтвердить что ты не робот

Являются ли принципиальные категории важными?

Однажды я задал вопрос о начинающих haskell, использовать ли данные /newtype или typeclass. В моем конкретном случае оказалось, что не требуется никаких классов. Кроме того, Том Эллис дал мне блестящий совет, что делать, когда вы сомневаетесь:

Самый простой способ ответить на это, который в основном правильный:

  использовать данные

Я знаю, что typeclasses может сделать несколько вещей немного красивее, но не так много AFIK. Мне также кажется, что модели обычно используются для работы с мозговыми стволовыми материалами, но в новых новинках новые стикеры вряд ли когда-либо появляются, и все делается с помощью данных /newtype.

Теперь я задаюсь вопросом, есть ли случаи, когда typeclasses абсолютно необходимы, и вещи не могут быть выражены с помощью data/newtype?

Отвечая на аналогичный вопрос на qaru.site/info/182447/..., Габриэль Гонзалес сказал

Используйте классы типов, если:

  Существует только одно правильное поведение для заданного типа

  Класс типа имеет связанные уравнения (т.е. "Законы" ), что все экземпляры должны удовлетворять

Хмм..

Или это типы и типы данных /newtype несколько конкурирующих концепций, которые сосуществуют по историческим причинам?

4b9b3361

Ответ 1

Я бы сказал, что классные классы являются неотъемлемой частью Haskell.

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

Итак, расскажите о передаче словаря.

Теперь любой вид передачи словаря является большим улучшением состояния дел на традиционных объектно-ориентированных языках. Мы знаем, как делать OOP с помощью vtables в С++. Тем не менее, vtable является "частью объекта" на языках ООП. Слияние vtable с объектом заставляет ваш код в форму, где у вас жесткая дисциплина о том, кто может расширять основные типы новыми функциями, а на самом деле это только оригинальный автор этого класса, который должен включать в себя все вещи, которые другие хотят испечь в их тип. Это приводит к "коду потока лавы" и всевозможным другим антипаттернам дизайна и т.д.

Языки, подобные С#, дают вам возможность взломать методы расширения для подделки нового материала, а "черты" на таких языках, как scala и множественное наследование на других языках, позволяют вам делегировать часть работы, но они частично решения.

Когда вы разбиваете vtable на объекты, с которыми они манипулируют, вы получаете пьяный прилив силы. Теперь вы можете передавать их туда, где хотите, но тогда, конечно, вам нужно называть их и говорить о них. Этот подход использует дисциплина ML вокруг модулей/функторов и явный стиль передачи словаря.

Typeclasses придерживаются немного другого подхода. Мы полагаемся на уникальность экземпляра typeclass для данного типа, и в значительной степени именно этот выбор позволяет нам избегать таких простых типов данных ядра.

Почему?

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

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

Для простых примеров, таких как Set, вы можете избавиться от перевода ручного словаря. Цена не кажется такой высокой. Вы должны испечь в словаре, например, как вы хотите отсортировать Set при создании объекта, а затем insert/lookup, просто сохраните ваш выбор. Это может стоить вам дорого. Когда вы объединяете два Set, теперь, конечно, вверх по воздуху, который приказывает вам получить. Может быть, вы возьмете меньший размер и вставьте его в большую, но тогда порядок изменится волей-неволей, поэтому вместо этого вы должны принять слово, левое и всегда вставлять его вправо или документировать это случайное поведение. В настоящее время вы вынуждены вводить субоптимальные решения в интересах "гибкости".

Но Set - тривиальный пример. Там вы можете испечь индекс в типе о том, какой экземпляр он использовал, есть только один класс. Что происходит, когда вам нужно более сложное поведение? Одна из вещей, которые мы делаем с Haskell, - это работа с монадными трансформаторами. Теперь у вас много случаев, когда вы плаваете вокруг - и у вас нет хорошего места для их хранения, MonadReader, MonadWriter, MonadState и т.д. Все могут применяться.. условно, на основе лежащей в основе монады, что происходит, когда вы поднимаете и заменяете его, и теперь разные вещи могут или не могут применяться?

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

Это те вещи, которые упрощают работу классов.

Я полагаю, что вы должны использовать их для всего?

Недолго.

Но я не могу согласиться с другими ответами здесь, что они несущественны для Haskell.

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

Я согласен с некоторыми вещами здесь, использую классные классы, когда есть законы и когда выбор однозначен.

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

Ответ 2

Типовые разряды в большинстве случаев несущественны. Любой код класса можно механически преобразовать в словарь, проходящий. Они в основном обеспечивают удобство, иногда существенное удобство (см. Ответ kmett).

Иногда свойство single-instance классов типов используется для принудительного применения инвариантов. Например, вы не смогли бы безопасно преобразовать Data.Set в стиль передачи словаря, потому что если вы дважды вставляете два разных словаря Ord, вы можете разбить инвариант структуры данных. Конечно, вы все равно можете преобразовать любой рабочий код в рабочий код в стиле прокрутки в словаре, но вы не сможете запретить столь же сломанный код.

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

Ответ 3

Чтобы ответить на эту часть вопроса:

", а также типы данных /newtype несколько конкурирующих концепций"

Нет. Typeclasses являются расширением системы типов, что позволяет создавать ограничения на полиморфные аргументы. Как и большинство вещей в программировании, они, конечно же, синтаксический сахар [поэтому они не являются существенными в том смысле, что их использование не может быть заменено чем-либо еще]. Это не значит, что они лишние. Это просто означает, что вы могли бы выражать подобные вещи, используя другие языковые средства, но вы потеряете некоторую ясность, пока будете на нем. Передача словаря может использоваться в основном для одних и тех же вещей, но в конечном счете она менее строгая в системе типов, поскольку она позволяет изменять поведение во время выполнения (что также является отличным примером того, где вы должны использовать словарь, вместо классов классов).

Данные и newtype все равно означают то же самое, независимо от того, есть ли у вас типы типов или нет: введите новый тип в случае data как новый вид структуры данных, а в случае newtype - как типичный вариант type.

Ответ 4

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

Ответ 5

Я просто хочу сделать очень мирский вопрос о синтаксисе.

Люди склонны недооценивать удобство, предоставляемое типами классов, возможно потому, что они никогда не пробовали Haskell без использования каких-либо. Это "трава зеленее на другой стороне забора". Это явление.

while :: Monad m -> m Bool -> m a -> m ()
while m p body = (>>=) m p $ \x ->
                 if x
                 then (>>) m body (while m p body)
                 else return m ()

average :: Floating a -> a -> a -> a -> a
average f a b c = (/) f ((+) (floatingToNum f) a ((+) (floatingToNum f) b c))
                        (fromInteger (floatingToNum f) 3)

Это историческая мотивация для классов типов, и она остается актуальной и сегодня. Если бы у нас не было классов классов, нам бы, конечно, понадобилась какая-то замена, чтобы избежать написания таких чудовищ. (Может быть, что-то вроде каламбуров или Агды "открыто".)

Ответ 6

Я знаю, что typeclasses может сделать несколько вещей немного красивее, но не намного AFIK.

Бит лучше? Нет! Лучше! (как уже отмечали другие)

Однако ответ на это действительно сильно зависит от того, откуда этот вопрос возникает.

  • Если Haskell - ваш инструмент выбора для серьезной разработки программного обеспечения, мощный и существенный.
  • Если вы новичок, использующий haskell для learn (функциональный) программирования, сложность и сложность типов классов могут перевесить преимущества - конечно, в начале ваших исследований.

Вот несколько примеров сравнения ghc с gofer (предшественник объятий, предшественник современного haskell):

побегушках

? 1 ++ [2,3,4]
ERROR: Type error in application
*** expression     :: 1 ++ [2,3,4]
*** term           :: 1
*** type           :: Int
*** does not match :: [Int]

Теперь сравните с ghc:

Prelude> 1 ++ [2,3,4]
:2:1:
    No instance for (Num [a0]) arising from the literal `1'
    Possible fix: add an instance declaration for (Num [a0])
    In the first argument of `(++)', namely `1'
    In the expression: 1 ++ [2, 3, 4]
    In an equation for `it': it = 1 ++ [2, 3, 4]

:2:7:
    No instance for (Num a0) arising from the literal `2'
    The type variable `a0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Num Double -- Defined in `GHC.Float'
      instance Num Float -- Defined in `GHC.Float'
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus three others
    In the expression: 2
    In the second argument of `(++)', namely `[2, 3, 4]'
    In the expression: 1 ++ [2, 3, 4] 

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

Можно пройти весь путь (в gofer) и использовать "простую прелюдию", которая использует никаких моделей нет. Это делает его совершенно нереалистичным для серьезного программирования но настоящий аккуратный для обертывания головы вокруг Хиндли-Милнера:

Стандартная прелюдия

? :t (==)
(==) :: Eq a => a -> a -> Bool
? :t (+)
(+) :: Num a => a -> a -> a

Простая прелюдия

? :t (==)
(==) :: a -> a -> Bool
? :t (+)
(+) :: Int -> Int -> Int