Класс типов с одним параметром - это класс типов , который можно рассматривать как набор типов. Если Sub
является подклассом (подтип-классом) Super
, то набор типов, реализующих Sub
, является подмножеством из (или равным) набора типов, реализующих Super
. Все Monad
с Applicative
с, и все Applicative
с Functor
с.
Все, что вы можете делать с подклассами, вы можете делать с экзистенциально определенными количественными типами с типами в Haskell. Это потому, что они по сути одно и то же: в типичном языке ООП каждый объект с виртуальными методами содержит указатель vtable , который совпадает с указателем «словаря», который хранится в экзистенциально квантованном значении. с ограничением класса типов. Vtables являются существующими! Когда кто-то дает вам ссылку на суперкласс, вы не знаете, является ли он экземпляром суперкласса или подкласса, вы только знаете, что он имеет определенный интерфейс (либо из класса, либо из «интерфейса» ООП).
На самом деле вы можете сделать больше с обобщенными экзистенциалами Haskell. Мне нравится пример упаковки действия, возвращающего значение некоторого типа a
вместе с переменной, в которую будет записан результат после завершения действия; источник возвращает значение того же типа, что и переменная, но это скрыто извне:
data Request = forall a. Request (IO a) (MVar a)
Поскольку Request
скрывает тип a
, вы можете хранить несколько запросов разных типов в одном контейнере. Поскольку a
является полностью непрозрачным, то, что может сделать вызывающий абонент с Request
, only *, это запустить действие (синхронно или асинхронно) и записать результат в MVar
. Трудно использовать это неправильно!
Разница в том, что на языках ООП вы обычно можете:
Неявно upcast - использовать ссылку на подкласс, где ожидается ссылка на суперкласс, что должно быть сделано явно в Haskell (например, путем упаковки в экзистенциал)
Попытка понизить , что недопустимо в Haskell, если вы не добавите дополнительное ограничение Typeable
, в котором хранится информация о типе среды выполнения
Классы типов могут моделировать больше вещей, чем интерфейсы ООП и подклассы, однако, по нескольким причинам. Во-первых, поскольку они являются ограничениями для типов , а не объектов , вы можете иметь константы, связанные с типом, например mempty
в классе типов Monoid
:
class Semigroup m where
(<>) :: m -> m -> m
class (Semigroup m) => Monoid m where
mempty :: m
В языках ООП обычно нет понятия «статический интерфейс», который позволил бы вам выразить это. Будущая функция «концептов» в C ++ является ближайшим аналогом.
Другое дело, что подтипы и интерфейсы основаны на одном типе, тогда как у вас может быть класс типов с множественными параметрами, что обозначает набор кортежей типов. Вы можете думать об этом как о отношении . Например, набор пар типов, где один может быть приведен к другому:
class Coercible a b where
coerce :: a -> b
С функциональными зависимостями , вы можете сообщить компилятору о различных свойствах этого отношения:
class Ref ref m | ref -> m where
new :: a -> m (ref a)
get :: ref a -> m a
put :: ref a -> a -> m ()
instance Ref IORef IO where
new = newIORef
get = readIORef
put = writeIORef
Здесь компилятор знает, что отношение однозначное или функция : каждое значение «input» (ref
) отображается точно на одно значение «Вывод» (m
). Другими словами, если параметр ref
ограничения Ref
определен как IORef
, то параметр m
должен быть IO
- вы не можете иметь эту функциональную зависимость, а также отдельный экземпляр, отображающий IORef
на другую монаду, например instance Ref IORef DifferentIO
. Этот тип функциональных отношений между типами также может быть выражен с помощью связанных типов или более современных семейств типов (которые, на мой взгляд, обычно более понятны).
Конечно, это не идиоматичнопоздняя иерархия подклассов ООП напрямую в Haskell с использованием «экзистенциального типа антипаттерна», который обычно является избыточным. Часто существует гораздо более простой перевод, такой как ADT / GADT / records / functions - примерно это соответствует рекомендации ООП «предпочитать композицию наследованию».
В большинстве случаев, когда вы пишете класс в ООП, в Хаскеле обычно не требуется класс типов , а скорее модуль . Модуль, который экспортирует тип и некоторые функции, работающие с ним, по сути то же самое, что и открытый интерфейс класса, когда дело доходит до инкапсуляции и организации кода. Для динамического поведения, как правило, лучшим решением не является диспетчеризация на основе типов; вместо этого просто используйте функцию более высокого порядка. В конце концов, это функциональное программирование. :)