Выбор между классом и записью

основной вопрос: какие принципы проектирования следует соблюдать при выборе между использованием класса или использованием записи (с полиморфными полями) ?

во-первых, мы знаем, что классы и записи по существу эквивалентны (так как в Core классы десугарируются в словари, которые являются просто записями). Тем не менее, есть различия: классы передаются неявно, записи должны быть явными.

смотреть немного глубже, классы действительно полезны когда:

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

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

конечно, все становится запутаннее: что, если я хочу иметь ограничения на мои типы? Давайте выберем реальный пример:

class (Bounded i, Enum i) => Partition a i where
    index :: a -> i

я мог бы так же легко сделать

data Partition a i = Partition { index :: a -> i}

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

есть ли рекомендации по проектированию, что поможешь мне?

3 ответов


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

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

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


это взять два, расширяя немного на конкретный пример, но все-таки своего рода вращающиеся идеи...

Предположим, мы хотим смоделировать распределения вероятностей над реалами. На ум приходят два естественных представления.

а) Typeclass-управляемый

class PDist a where
        sample :: a -> Gen -> Double

B) словарь-driven

data PDist = PDist (Gen -> Double)

первый позволяет нам сделать

data NormalDist = NormalDist Double Double -- mean, var
instance PDist NormalDist where...

data LognormalDist = LognormalDist Double Double
instance PDist LognormalDist where...

последнее позволяет нам делать

mkNormalDist :: Double -> Double -> PDist...
mkLognormalDist :: Double -> Double -> PDist...

в первом мы можем написать

data SumDist a b = SumDist a b
instance (PDist a, PDist b) => PDist (SumDist a b)...

в последнем мы можем просто пиши

sumDist :: PDist -> PDist -> PDist

Итак, каковы компромиссы? Typeclass-driven позволяет нам указать, какие распределения нам даны. Компромисс заключается в том, что мы должны явно построить алгебру распределений, включая новые типы для их комбинаций. Управляемые данными не позволяют нам ограничить распределения, которые нам даны (или даже если они хорошо сформированы), но взамен мы можем делать все, что захотим.

кроме того, мы можем написать parseDist :: String -> PDist сравнительно легко, но мы должны пройти через некоторые ангст делать экв подход typeclass.

так что это, в некотором смысле, типизированный / нетипизированный статический / динамический компромисс на другом уровне. Однако мы можем дать ему поворот и утверждать, что типографский класс вместе с связанными алгебраическими законами определяет семантика распределения вероятностей. И тип PDist действительно может быть сделан экземпляром класса PDist. Между тем, мы можем смириться с использованием типа PDist (а не typeclass) почти везде, пока мышление его как iso к башне экземпляров и типов данных, необходимых для более "богатого" использования класса."

на самом деле, мы даже можем определить основную функцию PDist в условия функций typeclass. т. е. mkNormalPDist m v = PDist (sample $ NormalDist m v) таким образом, есть много места в пространстве дизайна, чтобы скользить между двумя представлениями по мере необходимости...


примечание: Я не уверен, что понимаю OP точно. Предложения/замечания по улучшению приветствуются!


Справочная информация:

когда я впервые узнал о typeclasses в Haskell, общее эмпирическое правило, которое я взял, было то, что по сравнению с Java-подобными языками:

  • typeclasses похожи на интерфейсы
  • data похожи на классы

здесь еще один вопрос SO и ответ, описывающие рекомендации по использованию интерфейсов (также некоторые недостатки чрезмерного использования интерфейса). Моя интерпретация:

  • записи / Java-классы-это то, что есть
  • интерфейсы / typeclasses-это роли, которые конкреция может выполнять
  • множественные, несвязанные конкреции могут выполнить такую же роль

держу пари, вы уже знаете все этот.


рекомендации, которые я пытаюсь следовать для моего собственного кода:

  • typeclasses предназначены для абстракций
  • записи предназначены для конкреций

поэтому на практике это означает:

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

пример:

typeclass Show С функцией show :: (Show s) => s -> String: для данных, которые могут быть представлены как String.

  • клиенты просто хотят превратить данные в строки
  • клиентам все равно, что такое данные (конкреция) - только забота о том, что его можно представить в виде строки
  • роль реализации данных: может быть string-ified
  • это не может быть достигнуто без typeclass - каждый тип данных потребует функции преобразования с другим именем, с какой болью иметь дело!

Type-classes иногда могут обеспечить дополнительную безопасность типа (примером может быть Ord С Data.Map.union). Если у вас есть аналогичные обстоятельства, когда выбор классов типов может помочь вашей безопасности типов, используйте классы типов.

я приведу другой пример, где я думаю, что классы типов не обеспечат дополнительную безопасность:

class Drawing a where
    drawAsHtml :: a -> Html
    drawOpenGL :: a -> IO ()

exampleFunctionA :: Drawing a => a -> a -> Something
exampleFunctionB :: (Drawing a, Drawing b) => a -> b -> Something

ничего exampleFunctionA и exampleFunctionB не мог сделать (мне трудно объяснить, почему, идеи добро пожаловать!--11-->).

в этом случае я не вижу пользы от использования типа-класса.

(отредактировано после обратной связи от Жака и вопроса от missingo)