【问题标题】:Difference between OOP interfaces and FP type classes [duplicate]OOP接口和FP类型类之间的区别[重复]
【发布时间】:2011-12-28 15:46:04
【问题描述】:

可能重复:
Java's Interface and Haskell's type class: differences and similarities?

当我开始学习 Haskell 时,我被告知类型类与接口不同且更强大。

一年后,我广泛使用了接口和类型类,但我还没有看到关于它们有何不同的示例或解释。这要么不是自然而然的启示,要么我错过了一些明显的东西,或者实际上没有真正的区别。

在互联网上搜索并没有发现任何实质性的东西。所以,你有答案吗?

【问题讨论】:

  • 想到两件事:实例可以事后添加(而不是作为类型定义的一部分),方法可以在返回值上重载(例如read)。
  • 最重要的是:接口是类型。类型类不是类型。

标签: oop haskell interface functional-programming typeclass


【解决方案1】:

您可以从多个角度看待这一点。其他人会不同意,但我认为 OOP 接口是理解类型类的好起点(当然与从无到有相比)。

人们喜欢指出,从概念上讲,类型类对类型进行分类,很像集合——“支持这些操作的类型集合,以及其他无法用语言本身编码的期望”。这是有道理的,并且偶尔会声明一个没有方法的类型类,说“只有在满足某些要求时才让你的类型成为这个类的实例”。对于 OOP 接口,这种情况很少发生1

就具体区别而言,类型类比 OOP 接口更强大的方式有多种:

  • 最大的一个是类型类将类型实现接口的声明与类型本身的声明分离。使用 OOP 接口,您可以在定义类型时列出该类型实现的接口,以后无法添加更多。对于类型类,如果您创建一个新的类型类,给定类型“模块层次结构”可以实现但不知道,您可以编写一个实例声明。如果你有一个类型和一个类型类来自不同的第三方,它们彼此不知道,你可以为它们编写一个实例声明。在与 OOP 接口类似的情况下,您大多只是被卡住了,尽管 OOP 语言已经发展出“设计模式”(适配器)来解决这个限制。

  • 下一个最大的问题(当然这是主观的)是,虽然从概念上讲,OOP 接口是一组可以在实现接口的对象上调用的方法,类型类是一组可以被与作为类成员的类型一起使用。区别很重要。因为类型类方法是根据类型而不是对象来定义的,所以使用具有多个该类型对象作为参数(相等和比较运算符)的方法,或者返回该类型对象作为结果的方法没有障碍(各种算术运算),甚至是类型的常量(最小和最大界限)。 OOP 接口无法做到这一点,而 OOP 语言已经演化出设计模式(例如虚拟克隆方法)来解决这个限制。

  • OOP 接口只能为类型定义;也可以为所谓的“类型构造函数”定义类型类。在各种 C 派生的 OOP 语言中使用模板和泛型定义的各种集合类型是类型构造函数:List 将类型 T 作为参数并构造类型 List<T>。类型类允许您为类型构造函数声明接口:例如,集合类型的映射操作,它在集合的每个元素上调用提供的函数,并将结果收集到集合的新副本中 - 可能具有不同的元素类型!同样,您不能使用 OOP 接口来执行此操作。

  • 如果给定参数需要实现多个接口,使用类型类很容易列出它应该属于哪些接口;对于 OOP 接口,您只能将单个接口指定为给定指针或引用的类型。如果您需要它来实现更多功能,您唯一的选择是没有吸引力的选择,例如在签名中编写一个接口并强制转换为其他接口,或者为每个接口添加单独的参数并要求它们指向同一个对象。您甚至无法通过声明一个继承自您需要的接口的新空接口来解决它,因为一个类型不会仅仅因为它实现了它的祖先而自动被视为实现了您的新接口。 (如果你可以在事后声明实现,这不会是一个问题,但是是的,你也不能这样做。)

  • 与上述情况相反,您可以要求两个参数具有实现特定接口的类型并且它们是相同的类型。对于 OOP 接口,您只能指定第一部分。

  • 类型类的实例声明更加灵活。对于 OOP 接口,你只能说“我声明了一个类型 X,它实现了接口 Y”,其中 X 和 Y 是特定的。使用类型类,您可以说“元素类型满足这些条件的所有 List 类型都是 Y 的成员”。 (你也可以说“所有属于 X​​ 和 Y 的类型也是 Z 的成员”,尽管在 Haskell 中这有很多问题。)

  • 所谓的“超类约束”比单纯的接口继承更灵活。对于 OOP 接口,您只能说“对于实现此接口的类型,它还必须实现这些其他接口”。这也是类型类最常见的情况,但超类约束也让你说诸如“SomeTypeConstructor 必须实现某某接口”或“应用于该类型的这种类型函数的结果必须满足某某约束”,等等。

  • 这是目前 Haskell 中的语言扩展(与类型函数一样),但您可以声明涉及多种类型的类型类。例如,同构类:类型对的类,您可以在其中从一个转换到另一个并返回而不会丢失信息。同样,OOP 接口也无法实现。

  • 我相信还有更多。

值得注意的是,在添加泛型的 OOP 语言中,可以消除其中一些限制(第四点、第五点,可能第二点)。

另一方面,OOP 接口可以做两件重要的事情,而类型类本身却不能:

  • 运行时动态调度。在 OOP 语言中,传递和存储指向实现接口的对象的指针并在运行时调用其上的方法是微不足道的,这些方法将根据对象的动态运行时类型进行解析。相比之下,默认情况下,类型类约束都是在编译时确定的——也许令人惊讶的是,在绝大多数情况下,这就是您所需要的。如果您确实需要动态调度,您可以使用所谓的存在类型(目前是 Haskell 中的语言扩展):一种结构,它“忘记”对象的类型,并且只记住(由您选择)它遵守某些类型类约束。从那时起,它的行为基本上与在 OOP 语言中实现接口的对象的指针或引用完全相同,并且类型类在这方面没有缺陷。 (需要指出的是,如果你有两个existentials实现同一个类型类,并且一个类型类方法需要它的类型的两个参数,你不能使用existentials作为参数,因为你不知道是否存在体具有相同的类型。但是与一开始就不能有这种方法的OOP语言相比,这并不损失。)

  • 对象到接口的运行时强制转换。在 OOP 语言中,您可以在运行时获取指针或引用并测试它是否实现了接口,如果实现了,则将其“强制转换”到该接口。类型类本身没有任何等效的东西(这在某些方面是一个优势,因为它保留了一个名为 parametricity 的属性,但我不会在这里讨论)。当然,没有什么能阻止你添加一个新的类型类(或扩充一个现有的类型类),使用方法将类型的对象转换为你想要的任何类型类的存在。 (您也可以更一般地以库的形式实现这种功能,但它涉及的内容要多得多。我保证有一天会完成它并将其上传到 Hackage!)

我应该指出,虽然您可以做这些事情,但许多人认为以这种方式模拟 OOP 是一种糟糕的风格,并建​​议您使用更直接的解决方案,例如函数的显式记录而不是类型类。有了完整的一流功能,该选项同样强大。

在操作上,OOP 接口通常通过在对象本身中存储一个或多个指针来实现,这些指针指向对象实现的接口的函数指针表。类型类通常是通过“字典传递”来实现的(对于像 Haskell 那样进行多态性的语言,而不是像 C++ 这样的多态性):编译器隐式地将指针传递给函数表(和常量) ) 作为每个使用类型类的函数的隐藏参数,并且无论涉及多少对象,该函数都会得到一个副本(这就是为什么你要做上面第二点中提到的事情的原因)。存在类型的实现看起来很像 OOP 语言所做的:指向类型类字典的指针与对象一起存储,作为“被遗忘”类型是其中成员的“证据”。

如果您曾经阅读过有关 C++ 的“概念”提案(因为它最初是针对 C++11 提出的),那么它基本上是针对 C++ 模板重新构想的 Haskell 类型类。我有时认为最好有一种语言,它只需要 C++ 概念,去掉一半的面向对象和虚函数,清理语法和其他缺陷,并在需要运行时添加存在类型基于类型的动态调度。 (更新:Rust 基本上就是这个,还有很多其他的好东西。)

1Serializable 在 Java 中是一个没有方法或字段的接口,因此是极少数情况之一。

【讨论】:

  • 可爱的帖子。不过,我希望明确提及您提到的 OOP 设计模式的名称。
  • 不幸的是,我的知识有点不足。适配器模式是缺少事后实例声明的一种解决方法,我在写第二点时正在考虑整个多态对象的克隆处理(你不能轻易指定输出应该是相同的type 作为输入),尽管我不确定这是否是实际的设计模式。如果有人有更好的信息,我会很乐意修改。
  • 这真是一个绝妙的答案。
  • 在详细阅读了您的帖子之后,类型类似乎与子类型类似。你能评论一下类型类和子类型之间的关系吗?有没有办法将类型类具体化为实际类型,而不是类型和值之外的东西?
  • @CMCDragonkai 第一个问题,值得一读:stackoverflow.com/questions/3848492/…
【解决方案2】:

我假设您在谈论 Haskell 类型的类。接口和类型类之间并没有真正的区别。顾名思义,类型类只是具有一组通用函数(以及相关类型,如果您启用了 TypeFamilies 扩展)的类型。

然而,Haskell 的类型系统本身就比 C# 的类型系统更强大。这允许您在 Haskell 中编写类型类,这是您无法在 C# 中表达的。即使是像Functor 这样简单的类型类也无法用C# 表示:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

C# 的问题是泛型本身不能是泛型的。换句话说,在 C# 中,只有 * 类型的类型可以是多态的。 Haskell 允许多态类型构造函数,因此任何类型的类型都可以是多态的。

这就是为什么 Haskell 中许多强大的泛型函数(mapMliftA2 等)无法在大多数类型系统功能较弱的语言中表达的原因。

【讨论】:

  • 并不是不能用 C# 或 Java 编写这些函数,而是用 C# 或 Java 编写它们的正确类型是不可能的,因为所有类型变量(“泛型”)都有 kind 0。
  • 如果我能接受两个答案,我也会选择这个。以及 +100 代表向我展示了这些实际上确实很重要:P
【解决方案3】:

主要区别 - 使类型类比接口更灵活 - 是类型类独立于其数据类型并且可以在之后添加。另一个区别(至少与 Java 不同)是您可以提供默认实现。一个例子:

//Java
public interface HasSize {
   public int size();
   public boolean isEmpty();
}

有这个接口很好,但是没有办法在不改变它的情况下将它添加到现有的类中。如果幸运的话,这个类是非最终的(比如ArrayList),所以你可以编写一个子类来实现它的接口。如果课程是最终的(比如String),那么你就不走运了。

将此与 Haskell 进行比较。你可以写类型类:

--Haskell
class HasSize a where
  size :: a -> Int
  isEmpty :: a -> Bool
  isEmpty x = size x == 0

您可以将现有的数据类型添加到类中而无需触及它们:

instance HasSize [a] where
   size = length

类型类的另一个不错的属性是隐式调用。例如。如果您在 Java 中有 Comparator,则需要将其作为显式值传递。在 Haskell 中,只要适当的实例在范围内,就可以自动使用等效的 Ord

【讨论】:

  • 我不确定我是否理解您的最后一点:“如果您在 Java 中有 Comparator,则需要将其作为显式值传递。在 Haskell 中,可以自动使用等效的 Ordering,如一旦合适的实例出现在范围内。”你能添加一个例子来澄清一下吗?
  • @staafl:如果你在 Java 中有一个 `Comparator`,当你想使用它时,你必须将它作为参数传递,例如在 `Collections.sort` 或 `new TreeSet. If you have an instance of Ord` in Haskell(几乎相同)的范围内,您可以调用Data.List.sort 没有这样的参数 - Haskell 将“找到" 自动为您提供正确的实例(如果没有,请抱怨),您仍然可以使用 Ord 中的函数。见learnyouahaskell.com/types-and-typeclasses#typeclasses-101
  • 这是一个奇怪的类比 - 在类型类和函数的默认参数之间。它可能在一个层面上起作用,但请记住,这只是一个类比 - 例如,每种类型无法拥有多种“比较器”,因为一个类型最多是一个类型类的实例。
  • @staafl 如果您考虑使用隐式参数的 Scala 及其“类型类模式”,这个类比就不那么奇怪了:如果您没有明确指定该参数,那么您有一个类似 haskell 的行为,如果您指定它,就像在 Java 中一样。参见例如blog.evilmonkeylabs.com/2012/06/11/…
猜你喜欢
  • 2013-03-29
  • 2011-04-07
  • 1970-01-01
  • 1970-01-01
  • 2014-05-30
  • 2011-01-05
  • 2021-08-28
  • 1970-01-01
  • 2016-11-28
相关资源
最近更新 更多