在学习 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 的影响?是否有任何文件/论文支持或反驳这一点?感谢所有的答案,他们都非常有启发性!
感谢所有输入!
我会说接口有点像类型类 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
这样的东西,因为您没有在这里调用函数,所以它不是方法。
接口和类型类之间的相似之处在于它们命名和描述了一组相关的操作。操作本身通过它们的名称、输入和输出来描述。同样,这些操作可能有许多实现,它们的实现可能会有所不同。
除此之外,这里有一些显着的差异:
接口方法总是与对象实例相关联。换句话说,总是有一个隐含的“this”参数,它是调用方法的对象。类型类函数的所有输入都是显式的。
接口实现必须定义为实现接口的类的一部分。相反,类型类“实例”可以与其关联类型完全分开定义......即使在另一个模块中也是如此。
总的来说,我认为可以公平地说类型类比接口更强大、更灵活。您将如何定义用于将字符串转换为某个值或实现类型的实例的接口?这当然不是不可能的,但结果不会是直观或优雅的。您是否曾经希望可以在某个编译库中为某个类型实现接口?这些都可以通过类型类轻松完成。
类型类被创建为一种结构化的方式来表达“临时多态性”,这基本上是重载函数的技术术语。类型类定义如下所示:
class Foobar a where
foo :: a -> a -> Bool
bar :: String -> a
这意味着,当您将函数 foo
应用于属于类 Foobar
的类型的某些参数时,它会查找特定于该类型的 foo
的实现,并使用它。这与 C++/C# 等语言中运算符重载的情况非常相似,只是更加灵活和通用。
接口在 OO 语言中服务于类似的目的,但底层概念有些不同; OO 语言带有 Haskell 根本没有的类型层次结构的内置概念,这在某些方面使事情复杂化,因为接口可能涉及通过子类型进行重载(即,在适当的实例上调用方法,子类型实现其超类型的接口)并通过基于平面类型的调度(因为实现接口的两个类可能没有同时实现它的公共超类)。鉴于子类型化引入的巨大额外复杂性,我建议将类型类视为非 OO 语言中重载函数的改进版本会更有帮助。
另外值得注意的是,类型类具有更灵活的调度方式——接口通常只适用于实现它的单个类,而类型类是为一个类型定义的,它可以出现在类函数签名的任何地方。 OO 接口中的等价物将允许接口定义将该类的对象传递给其他类的方法,定义静态方法和构造函数,这些方法和构造函数将根据调用上下文中所需的返回类型选择实现,定义方法采用与实现接口的类相同类型的参数,以及根本不真正翻译的各种其他东西。
简而言之:它们的用途相似,但它们的工作方式有些不同,并且类型类都更具表现力,并且在某些情况下,由于使用固定类型而不是继承层次结构的片段,因此使用起来更简单。
我已阅读上述答案。我觉得我可以稍微清楚一点地回答:
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# 可以用扩展方法来近似这样的东西。
在 Master minds of Programming 中,对类型类的发明者 Phil Wadler 进行了关于 Haskell 的采访,他解释了 Java 中的接口和 Haskell 中的类型类之间的相似之处:
像这样的 Java 方法: public static
因此,类型类与接口相关,但真正对应的是使用上述类型参数化的静态方法。
观看 Phillip Wadler 的演讲Faith, Evolution, and Programming Languages。 Wadler 在 Haskell 工作,是 Java 泛型的主要贡献者。
我不能说“炒作”级别,如果看起来不错的话。但是是的,类型类在很多方面都是相似的。我能想到的一个区别是 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);
}
}
它的工作方式是您只需要提供其中一种方法来实现接口。所以我想说,在接口级别提供一种你想要的行为的部分实现的能力是不同的。
阅读 Software Extension and Integration with Type Classes,其中给出了类型类如何解决接口无法解决的许多问题的示例。
论文中列举的例子有:
表达问题,
框架集成问题,
独立可扩展性问题,
主导分解、分散和纠缠的暴政。
它们是相似的(阅读:具有相似的用途),并且可能实现相似:Haskell 中的多态函数在引擎盖下采用一个“vtable”,列出与类型类关联的函数。
这个表通常可以在编译时推导出来。这在 Java 中可能不太正确。
但这是一个函数表,而不是方法表。方法绑定到一个对象,Haskell 类型类没有。
将它们视为 Java 的泛型。
正如 Daniel 所说,接口实现是从数据声明中单独定义的。正如其他人指出的那样,有一种直接的方法可以定义在多个地方使用相同自由类型的操作。因此很容易将 Num
定义为类型类。因此,在 Haskell 中,我们获得了运算符重载的语法优势,而实际上没有任何神奇的重载运算符——只是标准类型类。
另一个区别是您可以使用基于类型的方法,即使您还没有该类型的具体值!
例如,read :: Read a => String -> a
。因此,如果您有足够的其他类型信息来说明您将如何使用“读取”的结果,您可以让编译器确定为您使用哪个字典。
您还可以执行 instance (Read a) => Read [a] where...
之类的操作,它允许您为 any 可读内容列表定义一个读取实例。我认为这在 Java 中不太可能。
所有这一切都只是标准的单参数类型类,没有任何技巧。一旦我们引入了多参数类型类,就会打开一个全新的可能性世界,功能依赖和类型族更是如此,它们让您可以在类型系统中嵌入更多的信息和计算。