Класс типов с одним параметром — это класс типов, который можно представить как набор типов. Если 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
, — это запустить действие (синхронно или асинхронно) и записать результат в MVar
. Его трудно использовать неправильно!
Разница в том, что в языках ООП вы обычно можете:
Неявное восходящее преобразование — используйте ссылку на подкласс там, где ожидается ссылка на суперкласс, что должно быть сделано явно в 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
Здесь компилятор знает, что отношение является однозначным или функцией: каждое значение «входа» (ref
) отображается ровно на одно значение «выхода». (m
). Другими словами, если параметр ref
ограничения Ref
определяется как IORef
, то параметр m
должен быть IO
— у вас не может быть этой функциональной зависимости, а также отдельного экземпляра, отображающего IORef
на другую монаду. , например instance Ref IORef DifferentIO
. Этот тип функциональной связи между типами также может быть выражен с помощью связанных типов или более современных семейств типов (которые, на мой взгляд, обычно более понятны).
Конечно, не идиоматично переводить иерархию подклассов ООП непосредственно в Haskell, используя «антишаблон экзистенциального класса типов», который обычно является излишним. Часто существует гораздо более простой перевод, такой как ADT/GADT/записи/функции — примерно это соответствует совету ООП «предпочитать композицию наследованию».
В большинстве случаев, когда вы пишете класс в ООП, в Haskell вам обычно следует искать не класс типов, а скорее модуль. Модуль, который экспортирует тип и некоторые функции, работающие с ним, — это, по сути, то же самое, что и публичный интерфейс класса, когда речь идет об инкапсуляции и организации кода. Для динамического поведения, как правило, лучшим решением является не диспетчеризация на основе типов; вместо этого просто используйте функцию более высокого порядка. В конце концов, это функциональное программирование. :)
person
Jon Purdy
schedule
27.06.2018
Functor
в ООП? - person Mark Seemann   schedule 27.06.2018