OOP接口和FP类型类之间的区别

The*_*kle 59 oop haskell functional-programming interface typeclass

可能重复:
Java的接口和Haskell的类型类:差异和相似之处?

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

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

搜索互联网并没有发现任何实质性内容.那么,你有答案吗?

gla*_*erl 135

你可以从多个角度来看待这个问题.其他人会不同意,但我认为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语言中的接口的对象的行为完全相同,并且类型类在此区域中没有缺陷.(应该指出的是,如果你有两个存在实现相同的类型类,一个类型类方法需要两个类型的参数,你不能使用存在作为参数,因为你不知道是否存在主义具有相同的类型.但与OOP语言相比,首先不能有这样的方法,这不是一种损失.)

  • 将对象运行时转换为接口.在OOP语言中,您可以在运行时获取指针或引用,并测试它是否实现了接口,并将其"转换"为该接口(如果有).类型类本身没有任何等价物(这在某些方面是一个优点,因为它保留了一个称为参数化的属性,但我不会在这里讨论).当然,没有什么能阻止你添加一个新类型(或扩充现有类)的方法,将类型的对象强制转换为你想要的任何类型类的存在.(你也可以像图书馆一样更普遍地实现这样的功能,但它涉及的更多.我计划有一天完成它并将其上传到Hackage ,我保证!)

我应该指出,虽然你可以做这些事情,但许多人认为模仿OOP的方式不好,并建议你使用更直接的解决方案,例如显式记录函数而不是类型类.凭借完整的一流功能,该选项同样强大.

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

如果你曾经读过关于C++的"概念"提议(正如最初为C++ 11提出的那样),它基本上是Haskell为C++模板重新构想的类型类.我有时认为拥有一种简单地使用C++语言的概念会很好,它可以删除面向对象和虚函数的一半,清除语法和其他瑕疵,并在需要运行时添加存在类型基于类型的动态调度.(更新:Rust基本上是这个,还有许多其他好东西.)

1 Java中的Serializable是一个没有方法或字段的接口,因此很少出现.


小智 15

我假设您正在讨论Haskell类型类.它并不是接口和类型类之间的区别.正如名称所述,类型类只是一类具有一组通用函数的类型(如果启用了TypeFamilies扩展,则为相关类型).

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

class Functor f where
    fmap :: (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

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

这就是为什么许多强大的通用功能在Haskell(的原因mapM,liftA2等)不能在大多数语言表达有那么强大的类型系统.