ChatGPT解决这个技术问题 Extra ChatGPT

Java的Interface和Haskell的类型类:异同?

在学习 Haskell 时,我注意到它的 type 类,这应该是源自 Haskell 的一项伟大发明。

但是,在 the Wikipedia page on type class 中:

程序员通过指定一组函数或常量名称以及它们各自的类型来定义类型类,对于属于该类的每个类型都必须存在这些名称。

在我看来,这似乎与 Java 的接口 相当接近(引用 Wikipedia's Interface(Java) page):

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

这两个看起来很相似:类型类限制了一个类型的行为,而接口限制了一个类的行为。

我想知道 Haskell 中的类型类和 Java 中的接口之间有什么区别和相似之处,或者它们可能根本不同?

编辑:我注意到 even haskell.org admits that they are similar。如果它们如此相似(或者是吗?),那么为什么要这样炒作类型类呢?

更多编辑:哇,这么多好答案!我想我必须让社区来决定哪个是最好的。然而,在阅读答案时,他们似乎都只是说“typeclass 可以做很多事情,而 interface 不能或必须应对泛型”。我不禁想知道,有什么接口可以做而类型类不能做的吗?另外,我注意到 Wikipedia 声称 typeclass 最初是在 1989 年的论文 *“How to make ad-hoc polymorphism less ad hoc”中发明的,而 Haskell 仍处于摇篮中,而 Java 项目于 1991 年开始并于 1995 年首次发布. 所以也许不是 typeclass 类似于接口,相反,接口受 typeclass 的影响?是否有任何文件/论文支持或反驳这一点?感谢所有的答案,他们都非常有启发性!

感谢所有输入!

不,实际上没有什么接口可以做类型类不能做的事情,主要的警告是接口通常出现在具有 Haskell 中没有的内置功能的语言中。如果将类型类添加到 Java 中,它们也将能够使用这些特性。
如果你有多个问题,你应该问多个问题,而不是试图把它们都塞进一个问题中。无论如何,回答你的最后一个问题:Java 的主要影响是 Objective-C(而不是经常被错误报道的 C++),其主要影响依次是 Smalltalk 和 C。Java 的接口是对 Objective-C 协议的改编,这又是一个OO 中协议概念的形式化,这反过来又基于网络中的协议概念,特别是 ARPANet。这一切都发生在你引用的论文很久之前。 ...
... Haskell 对 Java 的影响要晚得多,而且仅限于泛型,毕竟泛型是由 Haskell 的一位设计师 Phil Wadler 共同设计的。
这是由 Java 的原始设计者之一 Patrick Naughton 撰写的 Usenet 文章:Java Was Strongly Influenced by Objective-C and not C++。不幸的是,它太旧了,以至于原始帖子甚至都没有出现在 Google 的档案中。
还有另一个问题作为这个问题的完全副本而被关闭,但它有一个更深入的答案:stackoverflow.com/questions/8122109/…

n
newacct

我会说接口有点像类型类 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 这样的东西,因为您没有在这里调用函数,所以它不是方法。


Scala 特征(基本上是接口)允许 this.type ,因此您可以返回或接受“自我类型”的参数。 Scala 有一个完全独立的特性,他们称之为“自我类型”,顺便说一句,这与此无关。这些都不是概念上的差异,只是实现上的差异。
S
Serid

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

除此之外,这里有一些显着的差异:

接口方法总是与对象实例相关联。换句话说,总是有一个隐含的“this”参数,它是调用方法的对象。类型类函数的所有输入都是显式的。

接口实现必须定义为实现接口的类的一部分。相反,类型类“实例”可以与其关联类型完全分开定义......即使在另一个模块中也是如此。

总的来说,我认为可以公平地说类型类比接口更强大、更灵活。您将如何定义用于将字符串转换为某个值或实现类型的实例的接口?这当然不是不可能的,但结果不会是直观或优雅的。您是否曾经希望可以在某个编译库中为某个类型实现接口?这些都可以通过类型类轻松完成。


你将如何扩展类型类?类型类可以像接口如何扩展接口一样扩展其他类型类吗?
鉴于 Java 8 在接口中的默认实现,可能值得更新此答案。
@CMCDragonkai 是的,例如,您可以说“class (Foo a) => Bar a where...”来指定 Bar 类型类扩展 Foo 类型类。和 Java 一样,Haskell 在这里也有多重继承。
在这种情况下,类型类与 Clojure 中的协议不同,但具有类型安全性?
这让我想知道类型类与 Rust 特征有何不同,因为它们也同时满足了两个要点
J
Joe the Person

类型类被创建为一种结构化的方式来表达“临时多态性”,这基本上是重载函数的技术术语。类型类定义如下所示:

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

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

接口在 OO 语言中服务于类似的目的,但底层概念有些不同; OO 语言带有 Haskell 根本没有的类型层次结构的内置概念,这在某些方面使事情复杂化,因为接口可能涉及通过子类型进行重载(即,在适当的实例上调用方法,子类型实现其超类型的接口)并通过基于平面类型的调度(因为实现接口的两个类可能没有同时实现它的公共超类)。鉴于子类型化引入的巨大额外复杂性,我建议将类型类视为非 OO 语言中重载函数的改进版本会更有帮助。

另外值得注意的是,类型类具有更灵活的调度方式——接口通常只适用于实现它的单个类,而类型类是为一个类型定义的,它可以出现在类函数签名的任何地方。 OO 接口中的等价物将允许接口定义将该类的对象传递给其他类的方法,定义静态方法和构造函数,这些方法和构造函数将根据调用上下文中所需的返回类型选择实现,定义方法采用与实现接口的类相同类型的参数,以及根本不真正翻译的各种其他东西。

简而言之:它们的用途相似,但它们的工作方式有些不同,并且类型类都更具表现力,并且在某些情况下,由于使用固定类型而不是继承层次结构的片段,因此使用起来更简单。


我一直在努力理解 Haskell 中的类型层次结构,你如何看待像 Omega 这样的类型系统类型?他们可以模拟类型层次结构吗?
@CMCDragonkai:我对 Omega 还不够熟悉,无法真正说对不起。
c
clay

我已阅读上述答案。我觉得我可以稍微清楚一点地回答:

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

Haskell 类型类是用与数据类型定义分开的“实例”实现的。在 C#/Java/Scala 中,接口/特征必须在类定义中实现。

Haskell 类型类允许您返回 this 类型或 self 类型。 Scala 特征也可以(this.type)。请注意,Scala 中的“自我类型”是一个完全不相关的功能。 Java/C# 需要使用泛型的混乱解决方法来近似此行为。

Haskell 类型类允许您在没有输入“this”类型参数的情况下定义函数(包括常量)。 Java/C# 接口和 Scala 特征要求所有函数都有一个“this”输入参数。

Haskell 类型类允许您定义函数的默认实现。 Scala 特征和 Java 8+ 接口也是如此。 C# 可以用扩展方法来近似这样的东西。


只是为了在这个答案点上添加一些文献,我今天阅读了 On (Haskell) Type Classes and (C#) Interfaces,它也与 C# 接口而不是 Java 进行了比较,但应该很好地在概念方面理解跨语言边界的接口概念。
我认为在 C# 中使用抽象类可能会更有效地完成默认实现之类的一些近似?
l
lambdas

Master minds of Programming 中,对类型类的发明者 Phil Wadler 进行了关于 Haskell 的采访,他解释了 Java 中的接口和 Haskell 中的类型类之间的相似之处:

像这样的 Java 方法: public static > T min (T x, T y) { if (x.compare(y) < 0) return x;否则返回 y; } 与 Haskell 方法非常相似: min :: Ord a => a -> a -> a min xy = if x < y then x else y

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


这应该是答案,因为根据Phil Wadler的主页,他是Haskell的主要设计者,同时他还设计了Java的Generics扩展,后来被包含在语言本身中。
m
mcandre

观看 Phillip Wadler 的演讲Faith, Evolution, and Programming Languages。 Wadler 在 Haskell 工作,是 Java 泛型的主要贡献者。


相关的东西在 25m 左右(虽然开头很有趣)。
C
Chris

我不能说“炒作”级别,如果看起来不错的话。但是是的,类型类在很多方面都是相似的。我能想到的一个区别是 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); 
    }
}

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


p
prayagupa

阅读 Software Extension and Integration with Type Classes,其中给出了类型类如何解决接口无法解决的许多问题的示例。

论文中列举的例子有:

表达问题,

框架集成问题,

独立可扩展性问题,

主导分解、分散和纠缠的暴政。


上面的链接死了。试试 this one instead
好吧,现在一个人也死了。试试this one
A
Alexandre C.

它们是相似的(阅读:具有相似的用途),并且可能实现相似:Haskell 中的多态函数在引擎盖下采用一个“vtable”,列出与类型类关联的函数。

这个表通常可以在编译时推导出来。这在 Java 中可能不太正确。

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

将它们视为 Java 的泛型。


s
sclv

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

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

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

您还可以执行 instance (Read a) => Read [a] where... 之类的操作,它允许您为 any 可读内容列表定义一个读取实例。我认为这在 Java 中不太可能。

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


普通类型类对于接口,就像多参数类型类对于 OOP 中的多分派一样;你获得了编程语言的能力和程序员的头痛的相应增加。