GADT提供的OOP和泛型无法做到什么?

时间:2015-04-05 05:48:03

标签: haskell generics ocaml gadt type-theory

GADTs 在函数式语言中是否等同于传统的 OOP +泛型,或者有一种情况是GADT很容易强制执行正确性约束但很难或不可能实现Java或C#?

例如,这个"良好类型的口译员" Haskell计划:

data Expr a where
  N :: Int -> Expr Int
  Suc :: Expr Int -> Expr Int
  IsZero :: Expr Int -> Expr Bool
  Or :: Expr Bool -> Expr Bool -> Expr Bool

eval :: Expr a -> a
eval (N n) = n
eval (Suc e) = 1 + eval e
eval (IsZero e) = 0 == eval e
eval (Or a b) = eval a || eval b

可以使用泛型和每个子类的适当实现在Java中等效编写,但更详细:

interface Expr<T> {
    public <T> T eval();
}

class N extends Expr<Integer> {
    private Integer n;

    public N(Integer m) {
        n = m;
    }

    @Override public Integer eval() {
        return n;
    }
}

class Suc extends Expr<Integer> {
    private Expr<Integer> prev;

    public Suc(Expr<Integer> aprev) {
        prev = aprev;
    }

    @Override public Integer eval() {
        return 1 + prev.eval()
    }
}


/** And so on ... */

2 个答案:

答案 0 :(得分:13)

OOP类是开放的,GADT是关闭的(就像普通的ADT一样)。

这里,“open”意味着您以后可以随时添加更多子类,因此编译器不能假定可以访问给定类的所有子类。 (有一些例外,例如Java的final但是它阻止了任何子类化,以及Scala的密封类)。相反,ADT在某种意义上是“封闭的”,你不能在以后添加更多的构造函数,并且编译器知道(并且可以利用它来检查例如详尽性)。有关详细信息,请参阅“expression problem”。

请考虑以下代码:

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是唯一可以生成tA t的{​​{1}}类型。关闭的GADT可以确保这一点。如果我们试图使用OOP类来模仿它,我们会失败:

B t

在这里,我认为除非采用像类型转换这样的不安全类型操作,否则我们无法实现相同的功能。 (此外,由于类型擦除,Java中的这些内容甚至不考虑参数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) { // ?? } 。)我们可能会考虑向TA添加一些通用方法以允许此操作,但是这样做会迫使我们为B和/或Int实施所述方法。

在这种特定情况下,人们可能只是诉诸非泛型函数:

String

或等效地向这些类添加非泛型方法。 但是,Char foo(A<Char> a, B<Char> b) // ... A之间共享的类型可能比单例B更大。更糟糕的是,类是开放的,所以只要添加一个新的子类,集合就会变大。

此外,即使您有Char类型的变量,您仍然不知道它是否为A<Char>,因此您无法访问A1的字段除了使用类型转换。这里输入的类型是安全的,因为程序员知道没有A1的其他子类。在一般情况下,这可能是错误的,例如。

A<Char>

此处data A a where A1 :: Char -> A Char A2 :: t -> t -> A t 必须是A<Char>A1的超类。


@gsg在关于平等证人的评论中提出要求。考虑

A2<Char>

这可以翻译为

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,BA=B}类由implements Teq<A,A>类实现。 界面需要从Teq1foo的转换函数A,以及B类型为trans的“及物性证明”thisTeq<A,B>类型的x可以生成对象Teq<B,C>。这是类似于上面使用GADT的Haskell代码的Java。

根据我的意见,Teq<A,C>时无法安全地实现该类:它需要返回空值或使用非终止作弊。

答案 1 :(得分:5)

泛型不提供类型相等约束。如果没有它们,你需要依靠垂头丧气,即失去类型安全。此外,某些调度 - 特别是访问者模式 - 不能安全地实现,并且具有适用于GADT的泛型的适当接口。请参阅本文,调查这个问题:

Generalized Algebraic Data Types and Object-Oriented Programming
安德鲁肯尼迪,克劳迪奥鲁索。 OOPSLA 2005。