В чем разница между типовой зависимостью в haskell и подтипированием в ООП? - PullRequest
0 голосов
/ 27 июня 2018

Мы часто используем зависимость класса типов для эмуляции отношения подтипа.

например:

когда мы хотим выразить отношение подтипа между Animal, Reptile и Aves в OOP:

abstract class Animal {
    abstract Animal move();
    abstract Animal hunt();
    abstract Animal sleep();
}

abstract class Reptile extends Animal {
    abstract Reptile crawl();
}

abstract class Aves extends Animal {
    abstract Aves fly();
}

мы можем перевести каждый абстрактный класс выше в класс типов в Haskell:

class Animal a where
    move :: a -> a
    hunt :: a -> a
    sleep :: a -> a

class Animal a => Reptile a where
    crawl :: a -> a

class Animal a => Aves a where
    fly :: a -> a

И даже когда нам нужен гетерогенный список, у нас есть ExistentialQuantification .

Так что мне интересно, почему мы до сих пор говорим, что на Haskell нет подтипирования, есть ли еще что-то, что может делать подтипирование, но нет типа class? Какая связь между ними и чем они отличаются?

1 Ответ

0 голосов
/ 27 июня 2018

Класс типов с одним параметром - это класс типов , который можно рассматривать как набор типов. Если 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. Трудно использовать это неправильно!

Разница в том, что на языках ООП вы обычно можете:

  1. Неявно upcast - использовать ссылку на подкласс, где ожидается ссылка на суперкласс, что должно быть сделано явно в Haskell (например, путем упаковки в экзистенциал)

  2. Попытка понизить , что недопустимо в 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 - примерно это соответствует рекомендации ООП «предпочитать композицию наследованию».

В большинстве случаев, когда вы пишете класс в ООП, в Хаскеле обычно не требуется класс типов , а скорее модуль . Модуль, который экспортирует тип и некоторые функции, работающие с ним, по сути то же самое, что и открытый интерфейс класса, когда дело доходит до инкапсуляции и организации кода. Для динамического поведения, как правило, лучшим решением не является диспетчеризация на основе типов; вместо этого просто используйте функцию более высокого порядка. В конце концов, это функциональное программирование. :)

...