Java 的 Interface 和 Haskell 的类型类: 差异和相似之处?

当我在学习 Haskell 的时候,我注意到了它的 类型类,它被认为是一个伟大的发明,起源于哈斯克尔。

然而,在 类型类的维基百科页面:

程序员通过指定一组函数或 必须存在的常量名及其各自的类型 对于属于该类的每个类型。

在我看来,这似乎与 Java 的接口相当接近(引用 维基百科界面(Java)页面) :

Java 编程语言中的接口是一种抽象类型 用于指定接口(在术语的通用意义上) 类必须实现的。

这两者看起来非常相似: type 类限制类的行为,而 interface 限制类的行为。

我想知道哈斯克尔的类型类和 Java 的接口之间有什么不同和相似之处,或者它们有什么本质上的不同?

编辑: 我注意到 就连 haskell.org 也承认它们很相似。如果它们如此相似(或者它们真的如此相似?),那么为什么类型类被如此大肆宣传?

更多编辑: 哇,这么多很棒的答案!我想我得让社区来决定哪个是最好的。然而,在阅读答案时,他们似乎都只是说 类型可以做很多事情,而接口不能或者必须处理泛型。我不禁想知道,有没有什么 接口能做什么而类型类不能做什么?。同时,我注意到,维基百科声称类型类最初是在1989年的论文 * “如何让 ad-hoc 多态性不那么 ad hoc”中发明的,当时 Haskell 还处于摇篮阶段,而 Java 项目始于1991年,并于1995年首次发布。那么,也许不是类型类类似于接口,而是相反,接口受到类型类的影响?是否有任何文件/论文支持或反驳这一点?感谢所有的答案,他们都是非常有启发性的!

感谢所有的投入!

21062 次浏览

接口和类型类之间的相似之处在于它们命名和描述一组相关操作。操作本身通过它们的名称、输入和输出进行描述。同样,这些操作的许多实现可能在实现上有所不同。

除此之外,还有一些值得注意的不同之处:

  • 接口方法总是与对象实例关联。换句话说,总是有一个隐含的‘ this’参数,它是调用方法的对象。类型类函数的所有输入都是显式的。
  • 接口实现必须定义为实现该接口的类的一部分。相反,类型类“实例”的定义可以与其关联的类型完全分离... ... 甚至在另一个模块中也是如此。

一般来说,我认为可以说类型类比接口更强大、更灵活。如何定义将字符串转换为实现类型的某个值或实例的接口?这当然不是不可能的,但结果不会是直观或优雅的。您是否曾经希望有可能在某些已编译的库中实现某种类型的接口?使用类型类可以很容易地实现这两个目标。

它们是相似的(读: 有相似的用途) ,并且可能实现相似: 哈斯克尔的多态函数在引擎盖下有一个“ vtable”,列出了与类型类相关的函数。

这个表通常可以在编译时推导出来,但在 Java 中可能不是这样。

但是这是一个 功能的表,而不是 方法。方法绑定到一个对象,而 Haskell 类型类没有。

将它们看作 Java 的泛型。

我认为接口类似于类 SomeInterface t,其中所有值的类型都是 t -> whatever(其中 whatever不包含 t)。这是因为使用 Java 和类似语言中的继承关系,所调用的方法取决于它们所调用的对象的类型,而不是其他任何东西。

这意味着很难让像 add :: t -> t -> t这样的东西具有一个接口,它对多个参数是多态的,因为接口无法指定方法的参数类型和返回类型与调用它的对象的类型(即“ self”类型)相同。对于泛型,有一些方法可以通过创建一个带有泛型参数的接口来伪造这种情况,这个接口的类型应该与对象本身相同,比如 Comparable<T>是如何做到这一点的,在这个接口中应该使用 Foo implements Comparable<Foo>,这样 compareTo(T otherobject)的类型应该是 t -> t -> Ordering。但是这仍然需要程序员遵循这个规则,而且当人们想要创建一个使用这个接口的函数时,他们必须有递归的泛型类型参数,这也会引起麻烦。

另外,不会有 empty :: t这样的函数,因为这里没有调用函数,所以它不是一个方法。

我不能说的“炒作”的水平,如果它似乎这样罚款。但是类型类在很多方面是相似的。我能想到的一个不同之处是 Haskell 可以为某些类型类的 行动提供行为:

class  Eq a  where
(==), (/=) :: a -> a -> Bool
x /= y     = not (x == y)
x == y     = not (x /= y)

这表明对于 Eq类型类的实例,有两个操作,等于 (==)和不等于 (/=)。但是不等运算是用等号来定义的(所以你只需要提供一个等号) ,反之亦然。

所以在可能不合法的 Java 语言中,类似于:

interface Equal<T> {
bool isEqual(T other) {
return !isNotEqual(other);
}


bool isNotEqual(T other) {
return !isEqual(other);
}
}

它的工作方式是你只需要提供其中一个方法来实现接口。所以我想说,在 接口级别上提供您想要的行为的部分实现的能力是不同的。

类型类是作为表示“ ad-hoc 多态性”的结构化方式创建的,这基本上是 重载函数的技术术语。类型类定义如下:

class Foobar a where
foo :: a -> a -> Bool
bar :: String -> a

这意味着,当您将函数 foo应用到属于类 Foobar的一些类型的参数时,它将查找特定于该类型的 foo实现,并使用该实现。这与 C + +/C # 等语言中的运算符重载非常相似,只是更加灵活和通用。

接口在面向对象语言中也有类似的用途,但是底层的概念有些不同; 面向对象语言带有内置的类型层次结构的概念,而 Haskell 根本没有,这在某些方面使问题复杂化,因为接口可能涉及子类型的重载(例如,在适当的实例上调用方法,子类型实现它们的超类型的接口)和基于平面类型的分派(因为两个实现接口的类可能没有一个同样实现接口的公共超类)。鉴于子类型带来的巨大的额外复杂性,我建议将类型类视为非 OO 语言中重载函数的改进版本更有帮助。

同样值得注意的是,类型类有更加灵活的分派方式——接口通常只适用于实现它的单个类,而类型类是为 类型定义的,它可以出现在类函数的签名中的任何地方。面向对象接口中的等价物是允许接口定义将该类的一个对象传递给其他类的方法,定义静态方法和构造函数,根据调用上下文时需要的 报税表类别选择一个实现,定义使用与实现接口的类相同类型的参数的方法,以及其他各种根本不需要翻译的东西。

简而言之: 它们的用途相似,但是它们的工作方式有所不同,类型类的表达能力明显更强,在某些情况下,使用起来也更简单,因为它们处理的是固定类型,而不是继承层次结构的一部分。

正如 Daniel 所说,接口实现是从数据声明定义的 单独谈。正如其他人指出的那样,有一种简单的方法可以定义在多个地方使用相同自由类型的操作。因此,很容易将 Num定义为类型类。因此,在哈斯克尔,我们得到了运算符重载的语法优势,而实际上没有任何神奇的超载操作符——只有标准的类型类。

另一个区别是,您可以使用基于类型的方法,即使您还没有该类型的具体值!

例如,read :: Read a => String -> a。因此,如果您有足够多的其他类型信息,关于如何使用“ read”的结果,您可以让编译器为您指定使用哪个字典。

您还可以做一些事情,比如 instance (Read a) => Read [a] where...,它允许您为 任何可读事物列表定义一个读实例。我觉得在爪哇不太可能。

所有这些都只是标准的单参数类型类,没有任何花招。一旦我们引入了多参数类型类,那么一个全新的可能性世界就会打开,函数依赖和类型族更是如此,它们可以让您在类型系统中嵌入更多的信息和计算。

请看 Phillip Wadler 的演讲 信念、进化与编程语言。 Wadler 在 Haskell 上工作,是 Java 泛型的主要贡献者。

阅读 类型类的软件扩展与集成,其中给出了类型类如何解决许多接口无法解决的问题的例子。

文件中列举的例子如下:

  • 表达问题,
  • 框架集成问题,
  • 独立扩展性问题,
  • 支配性分解、分散和纠缠的专制。

编程大师中,有一篇关于 Haskell 的采访,采访对象是类型类的发明者 Phil Wadler,他解释了 Java 中的接口和哈斯克尔的类型类之间的相似之处:

这样的 Java 方法:

   public static <T extends Comparable<T>> T min (T x, T y)
{
if (x.compare(y) < 0)
return x;
else
return y;
}

与 Haskell 方法非常相似:

   min :: Ord a => a -> a -> a
min x y  = if x < y then x else y

因此,类型类与接口相关,但真正的对应关系是使用上述类型参数化的静态方法。

我读过以上的答案,我觉得我可以回答得更清楚一些:

Haskell“ type class”和 Java/C # “ interface”或 Scala“ trait”基本上是类似的。它们之间没有概念上的区别,但在实施上有所不同:

  • Haskell 类型类是用独立于数据类型定义的“实例”实现的。在 C #/Java/Scala 中,接口/特性必须在类定义中实现。
  • Haskell 类型类允许返回 this 类型或 self 类型。Scala 特性也是如此(this. type)。请注意,Scala 中的“自我类型”是一个完全不相关的特性。Java/C # 需要使用泛型来近似处理这种行为。
  • Haskell 类型类允许您定义函数(包括常量) ,而不需要输入“ this”类型参数。Java/C # 接口和 Scala 特性要求所有函数都有一个“ this”输入参数。
  • Haskell 类型类允许您定义函数的默认实现。Scala 特性和 Java8 + 接口也是如此。C # 可以使用扩展方法来近似处理类似的情况。