【问题标题】:Achieving polymorphism in functional programming在函数式编程中实现多态性
【发布时间】:2011-06-25 13:42:12
【问题描述】:

我目前正在享受从面向对象语言到函数式语言的转变。这是一股清新的空气,我发现自己比以前更有效率了。

但是 - OOP 的一个方面我还没有在 FP 方面看到令人满意的答案,那就是多态性。即我有大量数据项,当它们被传递到某些函数时,需要以完全不同的方式进行处理。为了争论,假设有多种因素驱动多态行为,因此可能会成倍增加不同的行为组合。

在 OOP 中,使用多态可以相对较好地处理:通过组合+继承或基于原型的方法。

在 FP 中我有点卡在:

  • 编写或编写纯函数,通过对每个数据项的值进行分支来有效地实现多态行为——感觉就像组装一个巨大的条件甚至模拟一个虚拟方法表!
  • 以类似原型的方式将函数放入纯数据结构中 - 这似乎可行,但它是否也违反了将纯函数与数据分开定义的想法?

对于这种情况,推荐的功能方法是什么?还有其他好的选择吗?

【问题讨论】:

  • 我对这个问题很感兴趣,虽然答案都非常有帮助和有趣,但我相信他们可能会遗漏一个重点,即构建异构集合的能力(即迭代什么获得这种多态性的好处)。 IIRC,某些语言(如 Haskell)很难拥有真正的异构集合。那是对的吗?您能否在回答中考虑这一点?
  • 好问题!我参加聚会太晚了,但在我看来,您遇到了经典的表达式问题:OOP 允许轻松添加新数据,FP 更容易添加更多功能。我建议阅读this article
  • @Ashley 你可能会发现this answer 很有用。
  • 谢谢@dbaltor,两个非常有趣的资源。现在,我知道问题的名称 :-) 顺便说一句,我想我听过(或读过)Martin Odersky 根据第一个参考资料说,当需要轻松添加子类/类型而不重新定义所有功能时,应该使用 OO,和 FP 当您想在不修改类型的情况下轻松添加新功能(或类似的东西,希望我能找到原始来源)。 Rust Traits 可以弥合这种区别?
  • 嘿@AshleyAitken,对不起!之前没看到你的评论。不知道奥德斯基的评论,但你是正确的!这就是表达式问题的意义所在。我不知道如何将 FP 和 OO 组合成新的东西来解决这个问题。但是,Traits 的概念似乎完全属于 OO 领域,与 FP 无关。例如,在 Rust 中,Traits 带有自引用。我不是 Haskeller,但他们似乎提出了 Existencial Types 的概念来构建异构集合。

标签: oop language-agnostic prototype functional-programming polymorphism


【解决方案1】:

【讨论】:

    【解决方案2】:

    Mike,您的两种方法都完全可以接受,正如布朗博士所说,在 SICP 的第 2 章中讨论了每种方法的优缺点。第一个是在某处有一个大类型表,需要维护。第二个只是传统的单调度多态/虚函数表。

    方案没有内置系统的原因是使用错误的对象系统来解决问题会导致各种麻烦,那么如果您是语言设计者,该选择哪个?单一调度单一继承不能很好地处理“驱动多态行为的多种因素,因此可能会成倍增加许多不同的行为组合。”

    总而言之,构造对象的方法有很多种,SICP 中讨论的语言 scheme 只是为您提供了一个基本工具包,您可以从中构造您需要的对象。

    在真正的方案程序中,您将手动构建对象系统,然后使用宏隐藏相关的样板。

    在 clojure 中,您实际上有一个内置的预构建对象/调度系统,其中内置了多方法,与传统方法相比,它的优势之一是它可以调度所有参数的类型。您(显然)也可以使用层次结构系统为您提供类似继承的功能,尽管我从未使用过它,所以您应该使用它cum grano salis

    但如果您需要与语言设计者选择的对象方案不同的东西,您可以制作一个(或多个)适合的。

    这实际上就是您在上面提出的建议。

    构建你需要的东西,让它全部工作,用宏隐藏细节。

    FP 和 OO 之间的争论不是关于数据抽象是否不好,而是关于数据抽象系统是否是填充程序所有独立关注点的地方。

    “我认为编程语言应该允许定义新的数据类型。我不认为程序应该只包含新数据类型的定义。”

    【讨论】:

      【解决方案3】:

      以类似原型的方式将函数放入纯数据结构中 - 这似乎可行,但它是否也违反了将纯函数与数据分开定义的想法?

      如果虚拟方法分派是您想要解决问题的方法,那么这是一个完全合理的方法。至于从数据中分离功能,这显然是一个非功能性的概念。我认为函数式编程的基本原则是函数是数据。至于您对虚拟功能的模拟,我认为这根本不是模拟。它是一个虚函数表,完全可以。

      仅仅因为该语言没有内置 OOP 支持并不意味着应用相同的设计原则是不合理的 - 这只是意味着您必须编写更多其他语言提供的内置机制,因为你正在与你所使用的语言的自然精神作斗争。现代类型化的函数式语言确实对多态性有非常深入的支持,但它是一种非常不同的多态性方法。

      OOP 中的多态很像逻辑中的“存在量化”——多态值有一些运行时类型,但你不知道它是什么。在许多函数式编程语言中,多态更像是“通用量化”——多态值可以实例化为用户想要的任何兼容类型。它们是完全相同的硬币的两个面(特别是,它们交换位置取决于您是从“内部”还是“外部”查看函数),但在设计语言来“使硬币公平”,尤其是在存在其他语言特征的情况下,例如子类型或更高种类的多态性(多态性超过多态性类型)。

      如果有帮助,您可能希望将函数式语言中的多态性视为非常类似于 C# 或 Java 中的“泛型”,因为这正是 ML 和 Haskell 等多态性所青睐的类型。

      【讨论】:

      • 实际上,我还应该提到类型推断是均衡支持这两种方法的一个复杂因素。这可能是函数式语言没有朝着这个方向发展的最大原因。一种同时支持存在类型和通用类型的语言非常难以为其推断类型,而我们函数式程序员真的很喜欢我们的类型推断;)。
      • 谢谢...非常有趣!碰巧我使用的是 Lisp (Clojure),所以编译时类型推断不是问题:-) 但我仍然想保持严格的函数式风格
      【解决方案4】:

      谁说的

      将纯函数与数据分开定义

      是最佳实践吗?

      如果你想要多态对象,你需要对象。在函数式语言中,可以通过将一组“纯数据”与一组对该数据进行操作的“纯函数”粘合在一起来构造对象。即使没有类的概念,这也有效。从这个意义上说,一个类只不过是一段代码,它用相同的关联“纯函数”集构造对象。

      多态对象是通过用具有相同签名的不同函数替换对象的某些函数来构造的。

      如果您想了解更多关于如何在函数式语言(如 Scheme)中实现对象的信息,请阅读本书:

      Abelson / Sussman: "Structure and Interpration of Computer programs"

      【讨论】:

      • 感谢您的观点...您所说的一切都是有道理的,但是许多公认的智慧/代码示例似乎清楚地将代码和数据分开...例如“更喜欢作用于 1 个数据结构的 1000 个函数”,受 MVC 启发的设计模式等。嗯,也许我应该忽略收到的智慧,选择可行的方法.....
      • @mikera:MVC 不会将代码与数据分开——它会将您软件中的不同关注点分开。那是另一回事,因为它作用于完全不同的抽象层次。
      【解决方案5】:

      嗯,在 Haskell 中,您总是可以创建一个类型类来实现一种多态性。基本上,它定义了为不同类型处理的函数。例如 EqShow 类:

      data Foo = Bar | Baz
      
      instance Show Foo where
          show Bar = 'bar'
          show Baz = 'baz'
      
      main = putStrLn $ show Bar
      

      函数show :: (Show a) => a -> String 是为每个实例化类型类Show 的数据类型定义的。编译器会根据类型为您找到正确的函数。

      这允许更一般地定义函数,例如:

      compare a b = a < b
      

      适用于类型类Ord 的任何类型。这与 OOP 不完全一样,但您甚至可以像这样继承类型类:

      class (Show a) => Combinator a where
          combine :: a -> a -> String
      

      由实例定义实际功能,您只需定义类型 - 类似于虚函数。

      这并不完整,据我所知,许多 FP 语言没有特征类型类。 OCaml 没有,它把它推到它的 OOP 部分。而且 Scheme 没有任何类型。但在 Haskell 中,这是一种在一定范围内实现多态性的强大方法。

      更进一步,2010 标准的更新扩展允许类型族等。

      希望这对您有所帮助。

      【讨论】:

      • 谢谢 - 我一如既往地喜欢 Haskell 的优雅,但这不是在不同类型的 Foo 上有效地定义条件分支吗?当 Foo 变得极其复杂和/或包含大量嵌套结构时,这将如何扩展?
      • 实际上,与类型关联的类型类函数基本上作为隐藏字典传递给需要它们的函数,所以show (Show a) =&gt; a -&gt; ()实际上是show Show -&gt; a -&gt; ()Show的实例由提供适合 a 类型的编译器。
      • @mikera 你必须承认 Haskell 是一种惰性语言,所以编译器应该足够聪明,不会仅仅为了模式匹配而评估嵌套结构。但我不是 100% 确定那里。通常,对于模式匹配,您必须相信编译器会根据您的需要对其进行优化。 GHC 在那里做得很好。请记住,模式匹配的处理方式与 switch 或 if 分支不同。
      • 另外,我认为类型类是类型检查器而不是实际函数评估的问题,所以我不太明白为什么将类传递给函数?
      • 你应该阅读"OOP vs typeclasses"。为了快速理解类型类,我推荐阅读 HOPL-III 论文"A History of Haskell: Being Lazy With Class"
      猜你喜欢
      • 2018-09-20
      • 2015-12-10
      • 2012-11-08
      • 2020-10-04
      • 2010-11-21
      • 1970-01-01
      • 2021-06-04
      • 2010-09-09
      • 1970-01-01
      相关资源
      最近更新 更多