Ответ 1
OOP классы открыты, GADT закрыты (например, простые ADT).
Здесь "open" означает, что вы всегда можете добавить дополнительные подклассы позже, поэтому компилятор не может предполагать, что он имеет доступ ко всем подклассам данного класса. (Есть несколько исключений, например Java final
, которые, тем не менее, предотвращают любые подклассы и Scala закрытые классы). Вместо этого ADT "закрыты" в том смысле, что позднее вы не можете добавлять дополнительные конструкторы, и компилятор знает это (и может использовать его для проверки, например, исчерпывающей). Для получения дополнительной информации см. Проблему .
Рассмотрим следующий код:
data A a where
A1 :: Char -> A Char
A2 :: Int -> A Int
data B b where
B1 :: Char -> B Char
B2 :: String -> B String
foo :: A t -> B t -> Char
foo (A1 x) (B1 y) = max x y
Приведенный выше код основан на Char
, единственном типе t
, для которого можно создать как A t
, так и B t
. GADT, будучи закрытыми, могут обеспечить это. Если мы попытались сопоставить это с использованием классов ООП, мы не получим:
class A1 extends A<Char> ...
class A2 extends A<Int> ...
class B1 extends B<Char> ...
class B2 extends B<String> ...
<T> Char foo(A<T> a, B<T> b) {
// ??
}
Здесь я думаю, что мы не можем реализовать одно и то же, если не прибегать к небезопасным типам операций, например, к типам. (Более того, они в Java даже не рассматривают параметр t
из-за стирания типа.) Мы могли бы подумать о добавлении некоторого общего метода в A
или B
, чтобы это разрешить, но это заставило бы нас реализовать метод для Int
и/или String
.
В этом конкретном случае можно просто прибегнуть к не общей функции:
Char foo(A<Char> a, B<Char> b) // ...
или, что то же самое, добавить к этим классам не общий метод.
Однако типы, разделяемые между A
и B
, могут быть более крупными, чем singleton Char
. Хуже того, классы открыты, поэтому набор может увеличиться, как только добавится новый подкласс.
Кроме того, даже если у вас есть переменная типа A<Char>
, вы по-прежнему не знаете, есть ли это A1
или нет, и из-за этого вы не можете получить доступ к полям A1
, кроме как с помощью приведения типов. Приведенный здесь тип будет безопасен только потому, что программист не знает другого подкласса A<Char>
. В общем случае это может быть неверно, например
data A a where
A1 :: Char -> A Char
A2 :: t -> t -> A t
Здесь A<Char>
должен быть суперклассом как A1
, так и A2<Char>
.
@gsg спрашивает в комментарии о свидетелях равенства. Рассмотрим
data Teq a b where
Teq :: Teq t t
foo :: Teq a b -> a -> b
foo Teq x = x
trans :: Teq a b -> Teq b c -> Teq a c
trans Teq Teq = Teq
Это можно перевести как
interface Teq<A,B> {
public B foo(A x);
public <C> Teq<A,C> trans(Teq<B,C> x);
}
class Teq1<A> implements Teq<A,A> {
public A foo(A x) { return x; }
public <C> Teq<A,C> trans(Teq<A,C> x) { return x; }
}
Приведенный выше код объявляет интерфейс для всех пар типов A,B
, который затем реализуется только в случае A=B
(implements Teq<A,A>
) классом Teq1
.
Для интерфейса требуется функция преобразования foo
от A
до B
и "доказательство транзитивности" trans
, которое задано this
типа Teq<A,B>
и
a x
типа Teq<B,C>
может создать объект Teq<A,C>
. Это Java, аналогичный коду Haskell, использующий GADT справа.
Класс не может быть безопасно реализован при A/=B
, насколько я могу видеть: для него потребовалось бы либо возвращать нули, либо обманывать без прерывания.