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

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

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

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

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

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

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

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

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

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

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

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

Есть ли рекомендации по дизайну, которые помогут мне?

4b9b3361

Ответ 1

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

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

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


Это займет два, немного расширившись на конкретном примере, но все же просто разновидные идеи...

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

A) Управляемый типом

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

B) Словарь

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 позволяет нам указать, какие дистрибутивы нам даны. Компромисс заключается в том, что мы должны явно строить алгебру распределений, включая новые типы для их комбинаций. Привод данных не позволяет нам ограничивать распределяемые нами распределения (или даже если они хорошо сформированы), но взамен мы можем делать все, что хотим.

Кроме того, мы можем записать a parseDist :: String -> PDist относительно легко, но нам нужно пройти через некоторый топор, чтобы сделать equiv для подхода типа typeclass.

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

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

Ответ 2

Примечание. Я не уверен, что я точно понимаю ОП. Предложения/комментарии для улучшения оценены!


Фон:

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

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

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

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

Бьюсь об заклад, вы уже все это знаете.


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

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

Таким образом, на практике это означает:

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

Пример:

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

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

Ответ 3

Типовые классы иногда могут обеспечить дополнительную безопасность типа (пример будет 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 не могло (мне сложно объяснить, почему, идеи приветствуются).

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

(Отредактирована следующая обратная связь от Жака и вопрос от пропавших без вести)