14

関数型プログラミング(動的型システムを使用)でポリモーフィズムを使用するにはどうすればよいですか?

次の例を考えてみましょう(最初はOOPで、2番目はFPで)。プログラムは非常に単純です-図のリストがあり、それらすべてを描画する必要があります。異なる図は異なる描画アルゴリズムを使用します。

OOPでは簡単に実行できますが、FPで実行するにはどうすればよいですか?特に、Scheme、Clojure(コンパイル時に静的型を解決しない)のような動的型システムを備えた言語では?

簡単なコードを作成しました(ライブバージョンhttp://tinkerbin.com/0C3y8D9Z、「実行」ボタンを押します)。FPサンプルでif/elseスイッチを使用しましたが、これは非常に悪いアプローチです。どのようにしてそのような問題をよりよく解決することができますか?

サンプルはJavaScriptで作成されていますが、これは単純化を目的としたものであり、動的型付けシステムを備えた関数型言語でソリューションを確認することは興味深いことです。

OOP

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!')
})
4

5 に答える 5

15

あなたの「FP」バージョンは、私が慣用的なFPの例と考えるものではありません。FPでは、多くの場合、OOPでクラスとメソッドディスパッチを使用するバリアントとパターンマッチングを使用します。特に、drawすでに内部でディスパッチを実行している関数は1つだけです。

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機能)に触れる必要がないという意味で真実です。drawFPの方法では、関数と、存在する可能性のある他のすべての操作を実行して拡張する必要があります。

しかし、それらの人々が見落としているのは、その逆もまた真であるということです。FPソリューションは、OOソリューションではない方法で拡張可能です。つまり、既存の図形に新しい操作を追加する場合、図形の定義や既存の操作に触れる必要はありません。別の関数を追加するだけですが、OOでは、すべてのクラスまたはコンストラクターを変更して、新しい操作の実装を含める必要があります。

つまり、ここにはモジュール性という点で二元論があります。両方の軸に沿って同時に拡張可能にする理想は、文献では「表現の問題」として知られており、さまざまな解決策が存在しますが(特に関数型言語では)、通常はより複雑です。したがって、実際には、目前の問題で問題になる可能性が高いディメンションに応じて、1つのディメンションを決定することがよくあります。

機能バージョンには他にも利点があります。たとえば、マルチディスパッチまたはより複雑なケースの区別に簡単に対応できます。また、複雑でさまざまなケースが相互に関連しているアルゴリズムを実装する場合も、コードを1か所にまとめたい場合に適しています。経験則として、OOでビジターパターンを使い始めるときはいつでも、機能スタイルのソリューションがより適切でした(そしてはるかに簡単でした)。

いくつかのさらなる意見:

  • プログラム編成におけるこの異なる好みは、FPの中心的な考え方ではありません。さらに重要なのは、可変状態を阻止し、再利用性の高い高次の抽象化を促進することです。

  • OOコミュニティには、すべての古いアイデアに対して新しい(流行語)単語を発明するというこの習慣があります。「ポリモーフィズム」という用語の使用(他の場所での意味とは完全に異なります)は、そのような例の1つです。呼び出し先が何であるかを静的に知らなくても関数を呼び出すことができるということだけです。関数がファーストクラスの値である任意の言語でそれを行うことができます。その意味で、OOソリューションも完全に機能します。

  • あなたの質問はタイプとはほとんど関係がありません。慣用的なOOと慣用的なFPソリューションはどちらも、型なしまたは型付きの言語で機能します。

于 2012-11-20T21:22:37.100 に答える
4

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 ...
于 2012-11-20T17:48:12.427 に答える
4

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?"))

そして、いくつかのインスタンスに描画メソッドを適用できます

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?")
于 2012-11-20T23:52:15.677 に答える
2

主に関数型プログラミング用に設計されたいくつかの言語では、ポリモーフィズムと呼ばれるものとは異なりますが、(アドホックと呼ばれる)ポリモーフィズムを実現する方法があります。たとえば、Haskellには型クラスがあります(従来のOOPのクラスと混同しないでください)。

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

(Scalaにはオブジェクトがあり、型クラスを明らかに並列または超える暗黙のオブジェクトもあります。)次に、任意の数の独立した型を実装し、それぞれを型クラスのインスタンスにすることができます(これも独立して、たとえば完全に異なるモジュールで)。

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で使用するものです。一緒に属するケースの数が有限である場合(つまりsealed、OOPの代替でクラスを使用できる場合)、代数的データ型を使用する可能性があります(以下を参照)。

もう1つの方法は、JSスニペットが行うことを実行することです(ちなみに、各タイプのオブジェクトがいくつもあり、このバージョンでも同じ問題がある場合にポリモーフィズムを実現するために行うことではありません)。各オブジェクトでポリモーフィックな動作を行う関数。ある意味で、「OOP」スニペットはすでに機能しています。

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) = …
于 2012-11-20T17:08:17.250 に答える
2

最初のコードサンプルについては、実際には機能しないものは何もありません。オブジェクト指向をサポートしていない言語でも、同じことができます。つまり、関数を含むレコード/構造/マップを作成し、それらをリストに入れることができます。

関数が1つしかない単純な例では、のように関数のリストを直接作成することもできますobjects = [drawCircle, drawRectangle]

于 2012-11-20T17:01:44.820 に答える