OOP 接口和 FP 类型类的区别 [英] Difference between OOP interfaces and FP type classes

查看:34
本文介绍了OOP 接口和 FP 类型类的区别的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

When I started learning Haskell, I was told that type classes are different and more powerful than interfaces.

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

One year later, I've used interfaces and type classes extensively and I've yet to see an example or explanation of how they are different. It's either not a revelation that comes naturally, or I've missed something obvious, or there is in actual fact no real difference.

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

Searching the Internet hasn't turned up anything substantial. So SO, do you have the answer?

推荐答案

您可以从多个角度看待这个问题.其他人会不同意,但我认为 OOP 接口是理解类型类的好起点(当然与从零开始相比).

You can look at this from multiple angles. Other people will disagree, but I think OOP interfaces are a good place to start from to understand type classes (certainly compared to starting from nothing at all).

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

People like to point out that conceptually, type classes classify types, much like sets - "the set of types which support these operations, along with other expectations which can't be encoded in the language itself". It makes sense and is occasionally done to declare a type class with no methods, saying "only make your type an instance of this class if it meets certain requirements". That happens very rarely with OOP interfaces1.

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

In terms of concrete differences, there are multiple ways in which type classes are more powerful than OOP interfaces:

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

  • The biggest one is that type classes decouple the declaration that a type implements an interface from the declaration of the type itself. With OOP interfaces, you list the interfaces a type implements when you define it, and there's no way to add more later. With type classes, if you make a new type class which a given type "up the module hierarchy" could implement but doesn't know about, you can write an instance declaration. If you have a type and a type class from separate third parties which don't know about each other, you can write an instance declaration for them. In analogous cases with OOP interfaces, you're mostly just stuck, though OOP languages have evolved "design patterns" (adapter) to work around the limitation.

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

The next biggest one (this is subjective, of course) is that while conceptually, OOP interfaces are a bunch of methods which can be invoked on objects implementing the interface, type classes are a bunch of methods which can be used with types which are members of the class. The distinction is important. Because type class methods are defined with reference to the type, rather than the object, there's no obstacle to having methods with multiple objects of the type as parameters (equality and comparison operators), or which return an object of the type as a result (various arithmetic operations), or even constants of the type (minimum and maximum bound). OOP interfaces just can't do this, and OOP languages have evolved design patterns (e.g. virtual clone method) to work around the limitation.

OOP 接口只能为类型定义;还可以为所谓的类型构造函数"定义类型类.在各种 C 派生的 OOP 语言中使用模板和泛型定义的各种集合类型是类型构造函数:List 将类型 T 作为参数并构造类型 List>.类型类允许您为类型构造函数声明接口:比如说,集合类型的映射操作,它调用集合的每个元素上提供的函数,并将结果收集在集合的新副本中——可能具有不同的元素类型!同样,您不能使用 OOP 接口来做到这一点.

OOP interfaces can only be defined for types; type classes can also be defined for what are called "type constructors". The various collection types defined using templates and generics in the various C-derived OOP languages are type constructors: List takes a type T as an argument and constructs the type List<T>. Type classes let you declare interfaces for type constructors: say, a mapping operation for collection types which calls a provided function on each element of a collection, and collects the results in a new copy of the collection - potentially with a different element type! Again, you can't do this with OOP interfaces.

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

If a given parameter needs to implement multiple interfaces, with type classes it's trivially easy to list which ones it should be a member of; with OOP interfaces, you can only specify a single interface as the type of a given pointer or reference. If you need it to implement more, your only options are unappealing ones like writing one interface in the signature and casting to the others, or adding separate parameters for each interface and requiring that they point to the same object. You can't even resolve it by declaring a new, empty interface which inherits from the ones you need, because a type won't automatically be considered as implementing your new interface just because it implements its ancestors. (If you could declare implementations after the fact, this wouldn't be such a problem, but yeah, you can't do that either.)

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

Sort of the reverse case of the one above, you can require that two parameters have types that implement a particular interface and that they be the same type. With OOP interfaces you can only specify the first part.

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

Instance declarations for type classes are more flexible. With OOP interfaces, you can only say "I'm declaring a type X, and it implements interface Y", where X and Y are specific. With type classes, you can say "all List types whose element types satisfy these conditions are members of Y". (You can also say "all types which are members of X and Y are also members of Z", although in Haskell this is problematic for a number of reasons.)

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

So-called "superclass constraints" are more flexible than mere interface inheritance. With OOP interfaces, you can only say "for a type to implement this interface, it must also implement these other interfaces". That's the most common case with type classes as well, but superclass constraints also let you say things like "SomeTypeConstructor must implement so-and-so interface", or "results of this type function applied to the type must satisfy so-and-so constraint", and so on.

这是目前 Haskell 中的语言扩展(与类型函数一样),但您可以声明涉及多种类型的类型类.例如,同构类:类型对的类,您可以在其中相互转换,而不会丢失信息.同样,使用 OOP 接口是不可能的.

This is currently a language extension in Haskell (as are type functions), but you can declare type classes involving multiple types. For example, an isomorphism class: the class of pairs of types where you can convert from one to the other and back without losing information. Again, not possible with OOP interfaces.

我相信还有更多.

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

It's worth noting that in OOP languages which add generics, some of these limitations can be erased (fourth, fifth, possibly second points).

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

On the other side, there are two significant things which OOP interfaces can do and type classes natively don't:

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

  • Runtime dynamic dispatch. In OOP languages, it's trivial to pass around and store pointers to an object implementing an interface, and invoke methods on it at runtime which will be resolved according to the dynamic, runtime type of the object. By contrast, type class constraints are by default all determined at compile time -- and perhaps surprisingly, in the vast majority of cases this is all you need. If you do need dynamic dispatch, you can use what are called existential types (which are currently a language extension in Haskell): a construct where it "forgets" what the type of an object was, and only remembers (at your option) that it obeyed certain type class constraints. From that point, it behaves basically in the exact same way as pointers or references to objects implementing interfaces in OOP languages, and type classes have no deficit in this area. (It should be pointed out that if you have two existentials implementing the same type class, and a type class method which requires two parameters of its type, you can't use the existentials as parameters, because you can't know whether or not the existentials had the same type. But compared to OOP languages, which can't have such methods in the first place, this is no loss.)

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

Runtime casting of objects to interfaces. In OOP languages, you can take a pointer or reference at runtime and test whether it implements an interface, and "cast" it to that interface if it does. Type classes don't natively have anything equivalent (which is in some respects an advantage, because it preserves a property called parametricity, but I won't get into that here). Of course, there's nothing stopping you from adding a new type class (or augmenting an existing one) with methods to cast objects of the type to existentials of whichever type classes you want. (You can also implement such a capability more generically as a library, but it's considerably more involved. I plan to finish it and upload it to Hackage someday, I promise!)

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

I should point out that while you can do these things, many people consider emulating OOP that way bad style and suggest you use more straightforward solutions, such as explicit records of functions instead of type classes. With full first-class functions, that option is no less powerful.

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

Operationally, OOP interfaces are usually implemented by storing a pointer or pointers in the object itself which point to tables of function pointers for the interfaces the object implements. Type classes are usually implemented (for languages which do polymorphism-by-boxing, like Haskell, rather than polymorphism-by-multiinstantiation, like C++) by "dictionary passing": the compiler implicitly passes the pointer to the table of functions (and constants) as a hidden parameter to each function which uses the type class, and the function gets one copy no matter how many objects are involved (which is why you get to do the things mentioned in the second point above). The implementation of existential types looks a lot like what OOP languages do: a pointer to the type class dictionary is stored along with the object as "evidence" that the "forgotten" type is a member of it.

如果你曾经读过 C++ 的概念"提案(因为它最初是为 C++11 提出的),它基本上是为 C++ 的模板重新构想的 Haskell 类型类.有时我认为拥有一种简单地采用 C++ 概念的语言会很好,将面向对象和虚函数的一半撕掉,清理语法和其他缺陷,并在需要运行时添加存在类型基于类型的动态调度.(更新:Rust 基本上就是这个,还有很多其他的好东西.)

If you've ever read about the "concepts" proposal for C++ (as it was originally proposed for C++11), it's basically Haskell's type classes reimagined for C++'s templates. I sometimes think it would be nice to have a language which simply takes C++-with-concepts, rips out the object-oriented and virtual functions half of it, cleans up the syntax and other warts, and adds existential types for when you need runtime type-based dynamic dispatch. (Update: Rust is basically this, with many other nice things.)

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

1Serializable in Java is an interface without methods or fields and thus one of those rare occurrences.

这篇关于OOP 接口和 FP 类型类的区别的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆