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

时间:2011-11-14 13:12:17

标签: oop haskell interface functional-programming typeclass

  

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

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

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

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

3 个答案:

答案 0 :(得分:124)

您可以从多个角度来看待这个问题。其他人会不同意,但我认为OOP接口是一个从理解类型类开始的好地方(当然比起从没有任何东西开始)。

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

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

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

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

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

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

  • 按照上述方法的相反情况排序,您可以要求两个参数具有实现特定接口的类型,它们是相同的类型。使用OOP接口,您只能指定第一部分。

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

  • 所谓的&#34;超类约束&#34;比仅仅接口继承更灵活。使用OOP接口,您只能说&#34;对于实现此接口的类型,它还必须实现这些其他接口&#34;。这也是类型类最常见的情况,但超类约束也让你说像#34; SomeTypeConstructor必须实现某某某界面&#34;或&#34;这类结果应用于该类型的函数必须满足某某约束&#34;等等。

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

  • 我确定还有更多。

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

另一方面,OOP接口可以做两件重要的事情,并且本地输入类别不是:

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

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

我应该指出,当你可以做这些事情时,很多人会考虑以不好的方式模仿OOP,并建议你使用更简单的解决方案,例如显式的函数记录而不是类型类。凭借完整的一流功能,该选项同样强大。

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

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

Java中的

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

答案 1 :(得分:15)

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

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

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

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

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

答案 2 :(得分:4)

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

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

拥有这个界面很不错,但没有办法将它添加到现有的类而不更改它。如果幸运的话,这个类是非final的(比如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