【问题标题】:How to use polymorphism in functional programming? [closed]如何在函数式编程中使用多态性? [关闭]
【发布时间】:2012-11-08 19:22:14
【问题描述】:

如何在函数式编程中使用多态性(带动态类型系统)?

让我们考虑以下示例(首先在 OOP 中,然后在 FP 中)。程序非常简单 - 有图形列表,我们需要将它们全部绘制出来,不同的图形使用不同的绘制算法。

在 OOP 中可以轻松完成,但在 FP 中如何做到呢?特别是在具有动态类型系统的语言中,例如 Scheme、Clojure(在编译时没有静态类型解析)?

我创建了简单的代码(实时版本http://tinkerbin.com/0C3y8D9Z,按“运行”按钮)。我在 FP 示例中使用了 if/else 开关,但这是一种非常糟糕的方法。如何更好地解决这样的问题?

示例是用 JavaScript 编写的,但这只是为了简单起见,如果看到任何具有动态类型系统的函数式语言的解决方案将会很有趣。

面向对象

var print = function(message){document.write(message + "\n<br/>")}

// Object Oriented Approach.
var circle = {
  draw: function(){print("drawing circle ...")}
}
var rectangle = {
  draw: function(){print("drawing rectangle ...")}
}

var objects = [circle, rectangle]
objects.forEach(function(o){
  o.draw()
})

FP

var print = function(message){document.write(message + "\n<br/>")}

// Functional Approach.
var circle = {type: 'Circle'}
var drawCircle = function(){print("drawing circle ...")}

var rectangle = {type: 'Rectangle'}
var drawRectangle = function(){print("drawing rectangle ...")}

var objects = [circle, rectangle]
objects.forEach(function(o){
  if(o.type == 'Circle') drawCircle(o)
  else if(o.type == 'Rectangle') drawRectangle(o)
  else throw new Error('unknown type!')
})

【问题讨论】:

    标签: oop clojure functional-programming polymorphism


    【解决方案1】:

    您的第一个代码示例确实没有任何非功能性。即使在不支持面向对象的语言中,您也可以做同样的事情。也就是说,您可以创建一个包含函数的记录/结构/映射,然后将它们放入您的列表中。

    在您只有一个函数的简单示例中,您也可以直接创建一个函数列表,例如objects = [drawCircle, drawRectangle]。

    【讨论】:

    • 谢谢,嗯,是的,但是我看不出使用函数式编程的意义,它看起来像是对 OOP 的重新发明......
    • @AlexeyPetrushin 函数式编程是第一位的,所以如果有的话,OOP 就是对 FP 的重新发明。此外,在 OOP 方法依赖于可变状态的情况下,FP 方法将更加明显不同。
    【解决方案2】:

    在主要为函数式编程设计的几种语言中,有一些方法可以实现(即所谓的)多态性,尽管它们与您所说的多态性不同。例如,Haskell 有类型类(不要与经典 OOP 中的类混淆):

    class Draw a where
        draw :: a -> SomethingSomthing -- probably IO () for your example, btw
    

    (Scala 有对象,还有implicits,它们显然是平行的,甚至超过了类型类。) 然后,您可以实现任意数量的独立类型,并使每个类型成为类型类的实例(同样独立,例如在完全不同的模块中):

    data Circle = Circle Point Double -- center, radius
    data Rectangle = Rect Point Double Double -- center, height, width
    
    instance Draw Circle where
        draw (Circle center radius) = …
    instance Draw Rectangle where
        draw (Rect center height width) = …
    

    如果您确实需要这种程度的可扩展性,这可能是您在 Haskell 中使用的。如果您有有限数量的案例属于一起(即您可以在 OOP 替代方案中使用 sealed 类),您可能会使用代数数据类型(见下文)。

    另一种方法是做你的 JS sn-p 所做的事情(顺便说一下,如果你有任意数量的每种类型的对象,而不是你为实现多态性所做的事情,并且这个版本具有相同的问题):在每个对象中嵌入一个执行多态行为的函数。从某种意义上说,你的“OOP”sn-p 已经可以正常工作了。

    data Drawable = Drawable (Drawable -> SomethingSomething) {- other fields -}
    draw (Drawable draw) = draw Drawable
    

    虽然在静态语言中,这不允许不同的对象具有不同的属性。

    与您提出的一堆条件相比,一个更容易接受的替代方案是与代数数据类型进行模式匹配,但仍然相似且具有相同的限制(很难添加其他形状)。 Stackoverflow 上的其他答案已经很好地解释了这些,我将用这种风格给出这个具体的例子:

    data Shape = Circle {- see second snippet -}
               | Rect {- ditto -}
    
    draw (Circle center radius) = …
    draw (Rect center height width) = …
    

    【讨论】:

    • 谢谢,但所有这些示例 - Haskel、Skala 和类型模式匹配都是静态类型的,并在编译时解析函数。
    • 至于在每个对象中嵌入函数 - 在现实生活场景中(至少在我使用 ruby​​ 和 javascript 的经验中)有很多这样的多态方法(特别是如果您使用元编程/代码-一代),您不能将它们全部嵌入。
    • @AlexeyPetrushin 类型类“方法”(在我的示例中,draw :: Draw a =&gt; a -&gt; SomethingSomething)绑定较晚,因此在运行时而不是编译时调度。您可以拥有具有不同类型和存在类型的形状列表,在每个形状上调用 draw,并为每个形状获取正确的实现。
    • @AlexeyPetrushin 至于嵌入:是的,这就是为什么不管范式如何,它都是一种有缺陷的方法。我提到它是为了完整性,因为它反映了问题中的例子(它遇到了同样的问题)。
    【解决方案3】:

    OO 多态不是函数式编程的一部分。然而,一些函数式语言(例如 clojure)具有 oo 多态性。

    另一种多态是多方法

    (def circle {:type :circle
                 :radius 50})
    
    (def rectangle {:type :rectangle
                    :width 5
                    :height 10})
    
    (defmulti draw :type)
    
    (defmethod draw :circle [object]
      (println "circle: radius = " (:radius object)))
    
    (defmethod draw :rectangle [object]
      (println "rectangle: "
               "width = " (:width object)
               "height = " (:height object)))
    
    (doseq [o [rectangle circle]] (draw o))
    => rectangle:  width =  5 height =  10
       circle: radius =  50
    

    或者你可以使用函数式样式

    (defn circle [] (println "drawing circle ..."))
    (defn rectangle [] (println "drawing rectangle ..."))
    
    (def objects [circle rectangle])
    
    (doseq [o objects] (o))
    => drawing circle ...
       drawing rectangle ...
    

    【讨论】:

    • 在这种情况下,我认为您的代码中有错误,因为它没有意义。 (def objects [draw-circle draw-rectangle])???这些函数是在哪里定义的?
    • """OO 多态性不是函数式编程的一部分""" 也许,但形状的问题 - 这是一个普遍的问题,应该以某种方式解决。与功能样式一样 - draw-circle draw-rectangle 与 draw - 它消耗了最宝贵的资源 - 我们大脑中的额外记忆。
    • @DaoWen 没有错误。我们收集了两个函数。
    • 我知道函数是一流的,您可以在 Clojure 中创建函数向量 — 问题是您没有在任何地方定义函数。 draw-circle 是从哪里来的?您在任一 sn-p 中都只有 circle 和 draw,在任何地方都没有 draw-circle。
    • @mobyte - 实际上,我想我在这里看到了问题。 Alexey 的代码对于他的square 和circle 只有一个成员:draw。我认为他假设会涉及更多状态(例如 x 和 y 坐标),但为了简单起见,在示例中没有包括它们。如果他们真的只有那个单一的draw 方法,那么你是完全正确的,一流的功能是 100% 等效的。如果涉及到额外的状态,那么多方法更适合。
    【解决方案4】:

    您的“FP”版本不是我认为惯用的 FP 示例。在 FP 中,您经常使用变体和模式匹配,而在 OOP 中您将使用类和方法分派。特别是,您只有一个 draw 函数已经在内部进行了调度:

    var circle = {type: 'Circle'}
    var rectangle = {type: 'Rectangle'}
    
    var draw = function(shape) {
      switch (shape.type) {
        case 'Circle': print("drawing circle ..."); break
        case 'Rectangle': print("drawing rectangle ..."); break
      }
    }
    
    var objects = [circle, rectangle]
    objects.forEach(draw)
    

    (当然,那是 JavaScript。在函数式语言中,您通常有更优雅和简洁的语法,例如:

    draw `Circle    = print "drawing circle..."
    draw `Rectangle = print "drawing rectangle..."
    
    objects = [`Circle, `Rectangle]
    foreach draw objects
    

    )

    现在,一般的 OO 爱好者会看到上面的代码并说:“但是 OO 解决方案是可扩展的,而上面的不是!”从某种意义上说,您可以轻松地将新形状添加到 OO 版本中,而无需触及任何现有形状(或它们的 draw 函数),这是正确的。使用 FP 方式,您必须进入并扩展 draw 函数,以及可能存在的所有其他操作。

    但那些人没有看到的是,反过来也是正确的:FP 解决方案是可扩展的,而 OO 解决方案则不然!即,当您在现有形状上添加新的 操作 时,您无需触及任何形状定义,也无需触及现有操作。您只需添加另一个函数,而对于 OO,您必须修改每个类或构造函数以包含新操作的实现。

    也就是说,就模块化而言,这里存在二元论。沿着两个轴实现同时可扩展性的理想在文献中被称为“表达问题”,虽然存在各种解决方案(尤其是在函数式语言中),但它们通常更复杂。因此,在实践中,您通常需要决定一个维度,具体取决于哪个维度更可能对手头的问题很重要。

    功能版本还有其他优点。例如,它可以简单地扩展到多调度或更复杂的案例区分。在实现复杂的算法并且不同情况相互关联的情况下,它也是更可取的,这样您就希望将代码放在一个地方。根据经验,当您开始在 OO 中使用访问者模式时,函数式解决方案会更合适(而且容易得多)。

    一些进一步的评论:

    • 程序组织中的这种不同偏好并不是 FP 的中心思想。更重要的是不鼓励可变状态,并鼓励高度可重用的高阶抽象。

    • OO 社区有这样一种习惯,即为每个旧想法创造新(流行)词。它使用术语“多态性”(与其他地方的含义完全不同)就是一个这样的例子。它只是说能够在不静态知道被调用者是什么的情况下调用函数。您可以在函数是一等值的任何语言中做到这一点。从这个意义上说,您的 OO 解决方案也非常实用。

    • 您的问题与类型几乎没有关系。惯用的 OO 和惯用的 FP 解决方案都可以在无类型或有类型的语言中工作。

    【讨论】:

      【解决方案5】:

      在 Clojure 中,有一些协议提供与 Haskell 的类型类基本相同的临时多态性:

      (defprotocol shape (draw [e]))
      (defrecord circle [radius])
      (defrecord rectangle [w h])
      (extend-protocol shape 
          circle (draw [_] "I am a nice circle")
          rectangle (draw [_] "Can I haz cornerz please?"))
      

      您还可以扩展现有类型:

      (extend-protocol shape 
         String (draw [_] "I am not a shape, but who cares?"))
      

      然后你可以将draw方法应用到一些实例上

      user=> (map draw [(->circle 1) (->rectangle 4 2) "foo"])
      ("I am a nice circle" "Can I haz cornerz please?" "I am not a shape, but who cares?")
      

      【讨论】:

      • 除了 clojure 不需要存在量化来将这些放在列表中。
      猜你喜欢
      • 2018-09-20
      • 2011-06-25
      • 1970-01-01
      • 2010-09-13
      • 2021-12-08
      • 2018-06-23
      • 1970-01-01
      • 2020-10-21
      • 2011-05-05
      相关资源
      最近更新 更多