Java的接口和Haskell的类型类:有什么区别和相似之处?

135
在我学习Haskell时,我注意到它的类型类,这被认为是源自Haskell的伟大发明。然而,在类型类的维基百科页面上中表示:
程序员通过指定一组函数或常量名称及其各自的类型来定义类型类,该类必须存在于属于该类的每种类型中。
这对我来说似乎非常接近Java的接口(引用维基百科的Java接口页面):
Java编程语言中的接口是用于指定必须实现接口(在通用意义上)的类的抽象类型。
这两个看起来相似:类型类限制了类型的行为,而接口限制了类的行为。我想知道Haskell中的类型类和Java中的接口之间有什么区别和相似之处,或者它们本质上是不同的?
编辑:我注意到甚至haskell.org也承认它们很相似。如果它们如此相似(或者它们是这样吗?),那么为什么类型类受到如此大的关注?

更新:哇,有这么多优秀的答案!我想我必须让社区决定哪个是最好的。然而,在阅读这些答案时,它们似乎都只是说“类型类可以做很多事情,而接口不能或必须处理泛型”。我不禁想知道,接口能做什么而类型类做不了呢? 另外,我注意到维基百科声称类型类最初是在1989年的论文《如何使特定的多态性更不那么临时》中发明的,当时Haskell还处于起步阶段,而Java项目始于1991年,首次发布于1995年。所以也许与其说类型类类似于接口,不如说是接口受到了类型类的影响? 是否有任何支持或反驳这一观点的文件/论文?感谢所有的答案,它们都非常启发人!

感谢所有的输入!


3
不,接口不能做任何类型类无法做到的事情,但需要注意的是接口通常出现在具有Haskell中没有的内置功能的语言中。如果在Java中添加了类型类,它们也可以使用这些功能。 - C. A. McCann
10
如果你有多个问题,应该提出多个问题,而不是试图将它们全部塞进一个问题里。回答你最后一个问题:Java的主要影响是Objective-C(与常常错误报道的C++不同),Objective-C 的主要影响则来源于Smalltalk和C。Java的接口是Objective-C的协议的一种适应,这又是OO中协议概念的正式化,而这个概念又基于网络中协议的想法,具体来说是ARPANet。这一切都发生在你引用的那篇论文之前很久。 - Jörg W Mittag
1
Haskell对Java的影响要晚得多,而且仅限于Generics,这毕竟是由Haskell的设计者之一Phil Wadler共同设计的。 - Jörg W Mittag
6
这是一篇由Java的原始设计师之一Patrick Naughton在Usenet发布的文章:Java受Objective-C的影响比受C++的影响强。不幸的是,这篇文章太旧了,以至于原始发布已经不在Google档案中出现。 - Jörg W Mittag
8
这里有一个问题被关闭,原因是与此问题完全重复,但它的答案更加深入:https://dev59.com/wGsz5IYBdhLWcg3wFT-_。 - Ben
显示剩余3条评论
10个回答

59
我认为接口有点像类型类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这样的函数,因为这里没有调用函数,所以它不是一个方法。

3
Scala特质(基本上是接口)允许使用this.type,因此您可以返回或接受“self type”的参数。顺便说一下,Scala有一个完全独立的功能,称为“self types”,与此无关。这些都不是概念上的区别,只是实现上的区别。 - clay

52
与接口和类型类相似之处在于,它们命名并描述了一组相关的操作。这些操作本身通过它们的名称、输入和输出来描述。同样,在这些操作中可能会有许多实现,这些实现可能在实现上略有不同。
现在,以下是一些显着的区别:
  • 接口方法始终与对象实例相关联。换句话说,始终存在一个隐含的“this”参数,它是调用该方法的对象。类型类函数的所有输入都是明确的。
  • 接口实现必须作为实现接口的类的一部分进行定义。相反,类型类“实例”可以完全与其关联的类型分开定义...甚至在另一个模块中定义。
总的来说,可以说类型类比接口更强大和灵活。你如何为将字符串转换为某个值或实现类型的实例定义接口?当然不是不可能,但结果不直观或优雅。您是否曾经希望能够为某个编译库中的类型实现接口?使用类型类很容易实现这两个功能。

1
你如何扩展类型类?类型类是否可以像接口扩展接口一样扩展其他类型类? - CMCDragonkai
12
鉴于Java 8接口中的默认实现,更新这个答案可能是值得的。 - Reinstate Monica
2
@CMCDragonkai 是的,你可以例如说“class (Foo a) => Bar a where...”来指定Bar类型类扩展了Foo类型类。像Java一样,Haskell在这里有多重继承。 - Reinstate Monica
在这种情况下,类型类是否与Clojure中的协议相同,但具有类型安全性? - nawfal
这让我想知道类型类与 Rust 特质的区别,因为它们都满足这两个要点。 - Post Self
在C#中,我们可以将一个类定义为“partial”,然后在其他地方实现一个该类不知道的接口。换句话说,你可以在另一个库中实现接口。这有意义吗?我不知道,但是这是可能的。 - Mohammad Azhdari

32

类型类是一种结构化的表达“特定多态”的方式,它基本上是指重载函数的技术术语。类型类定义看起来像这样:

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

这意味着,当您将函数foo应用于属于类Foobar的某些类型的参数时,它会查找特定于该类型的foo实现,并使用它。这与在C++/C#等语言中进行操作符重载的情况非常相似,但更灵活和通用。

接口在OO语言中具有类似的用途,但基本概念略有不同;OO语言带有一种内置的类型层级概念,而Haskell根本没有这种概念,这在某些方面使事情变得更加复杂,因为接口既涉及子类型重载(即,在适当的实例上调用方法,子类型实现其超类型所做的接口),又涉及扁平类型分派(因为实现接口的两个类可能没有共同的超类也实现它)。考虑到子类型引入的巨大附加复杂性,我建议把类型类看作是非OO语言中重载函数的改进版本,这更有助于理解。

还值得注意的是,类型类具有极为灵活的调度方式-接口通常仅适用于实现它的单个类,而类型类是针对类型定义的,可以出现在类函数的任何签名中。 OO接口中相当于这一点的是允许接口定义将该类的对象传递给其他类的方法,定义选择基于调用上下文中所需的返回类型的实现的静态方法和构造函数,定义接受与实现接口的类相同类型的参数的方法,以及各种其他根本无法转换的东西。

简而言之:它们具有类似的目的,但它们的工作方式略有不同,并且由于针对固定类型而不是继承层次结构的部分工作,类型类在某些情况下更加表达力强大,并且更加简单易用。


我一直在苦恼如何理解Haskell中的类型层次结构,你认为像Omega这样的类型系统能够模拟类型层次结构吗? - CMCDragonkai
@CMCDragonkai:抱歉,我对Omega不够熟悉,无法发表意见。 - C. A. McCann

19

我已阅读以上回答。我感觉我可以更清晰地回答:

Haskell的“类型类”、“Java/C#”接口或Scala“特质”基本上是类似的。它们之间没有概念上的区别,但有实现上的差异:

  • Haskell的类型类是通过与数据类型定义分离的“实例”来实现的。在C#/Java/Scala中,接口/特质必须在类定义中实现。
  • Haskell类型类允许您返回此类型或自身类型。Scala特征也是如此(this.type)。请注意,“Scala中的自身类型”是一个完全无关的功能。Java/C#需要使用泛型进行混乱的解决方法来近似此行为。
  • Haskell类型类允许您定义没有输入“this”类型参数的函数(包括常量)。Java/C#接口和Scala特质在所有函数上都需要“this”输入参数。
  • Haskell类型类允许您为函数定义默认实现。Scala特征和Java 8+接口也是如此。C#可以使用扩展方法近似某些内容。

2
只是为了给这个答案增加一些文献支持,我今天阅读了《(Haskell)类型类和(C#)接口》(http://joeduffyblog.com/2010/02/28/on-haskell-type-classes-and-c-interfaces),它也将C#接口与Java进行比较,但应该能够很好地理解在语言之间分解接口概念的概念侧面。 - daniel.kahlenberg
我认为在C#中,像默认实现这样的近似值可能会通过抽象类更有效地完成。 - Arwin

16
在《编程大师》一书中,Phil Wadler(类型类的发明者)接受了一次关于Haskell的采访,他解释了Java中接口和Haskell中类型类之间的相似之处。请参考原文

A Java method like:

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

is very similar to the Haskell method:

   min :: Ord a => a -> a -> a
   min x y  = if x < y then x else y
因此,类型类与接口相关,但真正对应的是上面参数化了类型的静态方法。

2
这应该是正确答案,因为根据Phil Wadler的个人主页,他是Haskell的主要设计者,同时也设计了Java的通用扩展Generics,后来被纳入了语言本身。 - Wong Jia Hau

13

1
相关的内容大约在25米左右(尽管开头很有趣)。 - Fred Foo

8

我不知道是否存在夸大其词的情况,如果有那也没关系。但是确实类型类在很多方面都是相似的。其中一个我能想到的区别是,在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); 
    }
}

它的工作方式是只需要提供其中一种方法来实现接口。因此,我认为在接口层面上提供部分行为的能力是一种区别。


8

请阅读《使用类型类进行软件扩展和集成》,其中列举了类型类如何解决接口无法解决的若干问题的示例。

本文中列举的例子包括:

  • 表达式问题,
  • 框架集成问题,
  • 独立可扩展性问题,
  • 主导分解、散布和缠结的暴政问题。

3
上面的链接已经失效,请尝试使用这个链接 - Justin Leitgeb
2
好吧,那个也挂了。试试这个 - RichardW

7
他们很相似(即:具有类似的用法),可能实现方式也相似:Haskell中的多态函数在底层采用一个“vtable”列表,列出了与类型类关联的函数。
这个表通常可以在编译时推断出来。但在Java中可能不是完全如此。
但这是一个关于函数而不是方法的表格。 方法绑定到对象,但Haskell类型类没有。
将它们视为Java的泛型。

4
正如Daniel所说,接口实现是与数据声明分开定义的。正如其他人指出的那样,有一种直接的方法可以定义使用相同自由类型的操作多次。因此,在Haskell中,我们获得了运算符重载的语法优势,而实际上并没有任何魔术重载的运算符,只有标准类型类。
另一个区别是,即使你没有具体的该类型的值,你也可以基于类型使用方法!
例如,read :: Read a => String -> a。因此,如果您拥有关于如何使用“读取”结果的足够的其他类型信息,您可以让编译器为您确定要使用哪个字典。
您还可以执行像instance (Read a) => Read [a] where...这样的操作,它允许您为任何可读事物列表定义读取实例。我认为在Java中不太可能做到这一点。
所有这些都仅仅是标准的单参数类型类,没有进行任何巧妙的处理。一旦引入多参数类型类,就会打开全新的可能性,尤其是功能依赖和类型族,它们让您在类型系统中嵌入更多的信息和计算。

1
普通类型类与接口的关系,就像多参数类型类与OOP中的多重分派的关系一样;你会在编程语言和程序员的头痛程度上获得相应的增加。 - C. A. McCann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接