首页 文章

OOP接口和FP类型类之间的区别[重复]

提问于
浏览
55

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

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

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

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

3 回答

  • 15

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

    人们喜欢在概念上指出类型类对类型进行分类,就像集合一样 - "the set of types which support these operations, along with other expectations which can't be encoded in the language itself" . 它是有道理的,偶尔会声明一个没有方法的类型类,说"only make your type an instance of this class if it meets certain requirements" . OOP接口1很少发生这种情况 .

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

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

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

    • OOP接口只能为类型定义;也可以为所谓的"type constructors"定义类型类 . 在各种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语言中,您可以在运行时获取指针或引用,并测试它是否实现了接口,如果是,则测试它是否为该接口的"cast" . 类型类本身没有任何等价物(这在某些方面是一个优点,因为它保留了一个名为parametricity的属性,但我赢了't get into that here). Of course, there'什么没有阻止你添加一个新的类类型(或扩充现有的类)与方法将类型的对象转换为你想要的任何类型类的存在 . (你也可以像图书馆一样更普遍地实现这种能力,但它涉及的更多 . 我计划有一天完成它并将它上传到Hackage,我保证!)

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

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

    如果你've ever read about the 3043473 proposal for C++ (as it was originally proposed for C++11), it' s基本上是Haskell 's type classes reimagined for C++'的模板 . 我有时认为有一种语言简单地采用C-with-concepts,将面向对象和虚函数分解为一半,清除语法和其他疣,并在需要运行时添加存在类型基于类型的动态调度 . (更新:Rust基本上是这个,还有许多其他好东西 . )

    Java中的1Serializable是一个没有方法或字段的接口,因此是罕见的事件之一 .

  • 4

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

    但是,Haskell 's type system is in itself more powerful than, for example, C#'的类型系统 . 这允许您在Haskell中编写类型类,您无法在C#中表达 . 即使像 Functor 这样简单的类型类也无法用C#表示:

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

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

    这就是为什么Haskell( mapMliftA2 等)中的许多强大的泛型函数无法用大多数语言表达的类型系统 .

  • 122

    主要区别 - 使类型类比接口更灵活 - 是类型类独立于其数据类型并且可以添加 afterwards . 另一个区别(至少对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 .

相关问题