Выбор между классом и записью - PullRequest
33 голосов
/ 12 ноября 2011

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

Во-первых, мы знаем, что классы и записи по существу эквивалентны (поскольку в 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 ]

6 голосов
/ 14 ноября 2011

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

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

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


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

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

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

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

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

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

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

4 голосов
/ 13 ноября 2011

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


Справка:

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

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

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

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

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


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

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

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

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

Пример:

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

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

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

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

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

...