32

私はプログラミング言語の設計を研究しており、一般的な単一ディスパッチ メッセージ パッシング OO パラダイムをマルチメソッド ジェネリック関数パラダイムに置き換える方法の問題に興味があります。ほとんどの場合、それは非常に簡単に思えますが、最近行き詰まってしまったので、助けていただければ幸いです。

私の考えでは、メッセージ パッシング OO は、2 つの異なる問題を解決する 1 つのソリューションです。次の疑似コードで、私が何を意味するかを詳しく説明します。

(1) ディスパッチの問題を解決します。

=== ファイル animal.code ===

   - Animals can "bark"
   - Dogs "bark" by printing "woof" to the screen.
   - Cats "bark" by printing "meow" to the screen.

=== ファイル内 myprogram.code ===

import animal.code
for each animal a in list-of-animals :
   a.bark()

この問題では、「樹皮」は、引数の型に応じて異なる動作をする複数の「分岐」を持つ 1 つのメソッドです。対象の引数の型 (Dogs と Cats) ごとに「bark」を 1 回実装します。実行時に、動物のリストを繰り返し処理し、適切な分岐を動的に選択できます。

(2) 名前空間の問題を解決します。

=== ファイル animal.code ===

   - Animals can "bark"

=== ファイル tree.code ===

   - Trees have "bark"

=== ファイル内 myprogram.code ===

import animal.code
import tree.code

a = new-dog()
a.bark() //Make the dog bark

…

t = new-tree()
b = t.bark() //Retrieve the bark from the tree

この問題では、「樹皮」は、実際には、たまたま同じ名前を持つ 2 つの概念的に異なる機能です。引数の型 (dog か tree か) によって、実際にどちらの関数を意味するかが決まります。


マルチメソッドは問題番号 1 をエレガントに解決します。しかし、問題番号 2 をどのように解決するかはわかりません。たとえば、上記の 2 つの例の最初の例は、単純な方法でマルチメソッドに変換できます。

(1) マルチメソッドを使用する犬と猫

=== ファイル animal.code ===

   - define generic function bark(Animal a)
   - define method bark(Dog d) : print("woof")
   - define method bark(Cat c) : print("meow")

=== ファイル内 myprogram.code ===

import animal.code
for each animal a in list-of-animals :
   bark(a)

キーポイントは、メソッド bark(Dog) が概念的に bark(Cat) に関連していることです。2 番目の例にはこの属性がありません。そのため、マルチメソッドが名前空間の問題を解決する方法がわかりません。

(2) マルチメソッドが動物と木で機能しない理由

=== ファイル animal.code ===

   - define generic function bark(Animal a)

=== ファイル tree.code ===

   - define generic function bark(Tree t)

=== ファイル内 myprogram.code ===

import animal.code
import tree.code

a = new-dog()
bark(a)   /// Which bark function are we calling?

t = new-tree
bark(t)  /// Which bark function are we calling?

この場合、ジェネリック関数はどこに定義する必要がありますか? animal と tree の両方の上にあるトップレベルで定義する必要がありますか? 2 つの関数は概念的に異なるため、動物と木の樹皮を同じジェネリック関数の 2 つのメソッドと考えるのは意味がありません。

私の知る限り、この問題を解決した過去の作品はまだ見つかっていません。Clojure マルチメソッドと CLOS マルチメソッドを見てきましたが、同じ問題があります。私は指を交差させて、問題に対するエレガントな解決策、または実際のプログラミングで実際に問題にならない理由についての説得力のある議論のいずれかを望んでいます。

質問に明確化が必要な場合はお知らせください。これはかなり微妙な(しかし重要な)ポイントだと思います。


返事の正気、Rainer、Marcin、および Matthias に感謝します。私はあなたの返信を理解し、動的ディスパッチと名前空間の解決が 2 つの異なるものであることに完全に同意します。CLOS は 2 つのアイデアを混同しませんが、従来のメッセージ パッシング OO は混同します。これにより、マルチメソッドを多重継承に簡単に拡張することもできます。

私の質問は、具体的には、融合が望ましい状況にあります。

以下は、私が言いたいことの例です。

=== ファイル: XYZ.code ===

define class XYZ :
   define get-x ()
   define get-y ()
   define get-z ()

=== ファイル: POINT.code ===

define class POINT :
   define get-x ()
   define get-y ()

=== ファイル: GENE.code ===

define class GENE :
   define get-x ()
   define get-xx ()
   define get-y ()
   define get-xy ()

==== ファイル: my_program.code ===

import XYZ.code
import POINT.code
import GENE.code

obj = new-xyz()
obj.get-x()

pt = new-point()
pt.get-x()

gene = new-point()
gene.get-x()

名前空間の解決とディスパッチが混同されているため、プログラマーは 3 つのオブジェクトすべてに対して単純に get-x() を呼び出すことができます。これも完全に明白です。各オブジェクトは独自のメソッド セットを「所有」しているため、プログラマーが何を意図したかについて混乱することはありません。

これをマルチメソッド バージョンと比較してください。


=== ファイル: XYZ.code ===

define generic function get-x (XYZ)
define generic function get-y (XYZ)
define generic function get-z (XYZ)

=== ファイル: POINT.code ===

define generic function get-x (POINT)
define generic function get-y (POINT)

=== ファイル: GENE.code ===

define generic function get-x (GENE)
define generic function get-xx (GENE)
define generic function get-y (GENE)
define generic function get-xy (GENE)

==== ファイル: my_program.code ===

import XYZ.code
import POINT.code
import GENE.code

obj = new-xyz()
XYZ:get-x(obj)

pt = new-point()
POINT:get-x(pt)

gene = new-point()
GENE:get-x(gene)

XYZ の get-x() は GENE の get-x() と概念的な関係がないため、これらは別のジェネリック関数として実装されています。したがって、エンド プログラマー (my_program.code 内) は get-x() を明示的に修飾し、実際にどのget-x() を呼び出すつもりかをシステムに伝える必要があります。

確かに、この明示的なアプローチはより明確であり、複数のディスパッチと複数の継承に簡単に一般化できます。しかし、名前空間の問題を解決するためにディスパッチを (悪用して) 使用することは、メッセージ パッシング OO の非常に便利な機能です。

個人的には、自分のコードの 98% が単一ディスパッチと単一継承を使用して適切に表現されていると感じています。私は複数のディスパッチを使用するよりも、名前空間の解決にディスパッチを使用するこの便利さを使用しているため、それをあきらめたくありません。

両方の長所を活かす方法はありますか? マルチメソッド設定で関数呼び出しを明示的に修飾する必要をなくすにはどうすればよいですか?


という意見が一致しているようです

  • マルチメソッドはディスパッチの問題を解決しますが、名前空間の問題には取り組みません。
  • 概念的に異なる関数には異なる名前を付ける必要があり、ユーザーはそれらを手動で修飾する必要があります。

したがって、単一継承の単一ディスパッチで十分な場合は、ジェネリック関数よりもメッセージ パッシング OO の方が便利だと思います。

これはオープンリサーチのようですね。名前空間の解決にも使用できるマルチメソッドのメカニズムを言語が提供する場合、それは望ましい機能でしょうか?

私はジェネリック関数の概念が好きですが、現在のところ、「些細なことを少し煩わしく」することを犠牲にして、「非常に難しいことをそれほど難しくない」ように最適化されていると感じています. コードの大部分は些細なものなので、これは解決する価値のある問題であると私は信じています。

4

7 に答える 7

21

動的ディスパッチと名前空間解決は 2 つの異なるものです。多くのオブジェクト システムでは、クラスは名前空間にも使用されます。また、多くの場合、クラスと名前空間の両方がファイルに関連付けられていることに注意してください。したがって、これらのオブジェクト システムは、少なくとも 3 つのことを混同しています。

  • スロットとメソッドを含むクラス定義
  • 識別子の名前空間
  • ソースコードの記憶単位

Common Lisp とそのオブジェクト システム (CLOS) の動作は異なります。

  • クラスは名前空間を形成しません
  • ジェネリック関数とメソッドはクラスに属さないため、クラス内で定義されていません
  • ジェネリック関数はトップレベル関数として定義されているため、ネストまたはローカルではありません
  • ジェネリック関数の識別子はシンボルです
  • シンボルには、パッケージと呼ばれる独自の名前空間メカニズムがあります
  • 一般的な機能は「オープン」です。いつでもメソッドを追加または削除できます
  • ジェネリック関数は第一級オブジェクトです
  • 数学はファーストクラスのオブジェクトです
  • クラスとジェネリック関数もファイルと混同されません。複数のクラスと複数のジェネリック関数を 1 つのファイルまたは必要な数のファイルに定義できます。また、実行中のコード (ファイルに関連付けられていない) や REPL (read eval print loop) などからクラスとメソッドを定義することもできます。

CLOS でのスタイル:

  • 機能が動的ディスパッチを必要とし、その機能が密接に関連している場合、異なるメソッドで 1 つの汎用関数を使用します
  • 多くの異なる機能があり、共通の名前が付いている場合は、それらを同じ汎用関数に入れないでください。さまざまな汎用関数を作成します。
  • 同じ名前のジェネリック関数ですが、名前が異なるパッケージにある場合は、異なるジェネリック関数です。

例:

(defpackage "ANIMAL" (:use "CL")) 
(in-package "ANIMAL")

(defclass animal () ())
(deflcass dog (animal) ())
(deflcass cat (animal) ()))

(defmethod bark ((an-animal dog)) (print 'woof))
(defmethod bark ((an-animal cat)) (print 'meow)) 

(bark (make-instance 'dog))
(bark (make-instance 'dog))

クラスANIMALとパッケージANIMALは同じ名前であることに注意してください。しかし、それは必要ありません。名前は決して接続されていません。DEFMETHOD は、対応する汎用関数を暗黙的に作成します。

別のパッケージ (たとえばGAME-ANIMALS) を追加すると、BARK一般的な機能が異なります。これらのパッケージが関連している場合を除きます (たとえば、あるパッケージが別のパッケージを使用している場合)。

別のパッケージ (Common Lisp のシンボル名前空間) から、これらを呼び出すことができます:

(animal:bark some-animal)

(game-animal:bark some-game-animal)

シンボルには構文があります

PACKAGE-NAME::SYMBOL-NAME

パッケージが現在のパッケージと同じ場合は、省略できます。

  • ANIMAL::BARKBARKパッケージで指定されたシンボルを参照しますANIMAL。コロンが 2 つあることに注意してください。
  • AINMAL:BARKパッケージ内のエクスポートされたシンボルを参照します。コロンが 1 つしかないことに注意してください。exportimport、およびusingは、パッケージとそのシンボルに対して定義されたメカニズムです。したがって、それらはクラスやジェネリック関数から独立していますが、それらを命名するシンボルの名前空間を構築するために使用できます。BARKANIMAL

より興味深いケースは、マルチメソッドが実際にジェネリック関数で使用される場合です。

(defmethod bite ((some-animal cat) (some-human human))
  ...)

(defmethod bite ((some-animal dog) (some-food bone))
  ...)

CAT上記では、 、HUMANDOGおよびのクラスを使用していますBONE。ジェネリック関数はどのクラスに属するべきですか? 特別な名前空間はどのようになりますか?

ジェネリック関数はすべての引数に対してディスパッチするため、ジェネリック関数を特別な名前空間と混同して単一のクラスで定義することは直接的な意味を持ちません。

動機:

汎用関数は、 Xerox PARC ( Common LOOPS用) およびSymbolics for New Flavorsの開発者によって、80 年代に Lisp に追加されました。追加の呼び出しメカニズム (メッセージの受け渡し) を取り除き、通常の (トップレベルの) 関数にディスパッチをもたらしたいと考えていました。新しいフレーバーには単一のディスパッチがありましたが、複数の引数を持つ汎用関数でした。その後、Common LOOPS の調査により、複数のディスパッチがもたらされました。その後、新しいフレーバーと共通のループは、標準化された CLOS に置き換えられました。その後、これらのアイデアはDylanなどの他の言語にもたらされました。

質問のサンプルコードは、汎用関数が提供するものを何も使用していないため、何かをあきらめなければならないようです。

単一のディスパッチ、メッセージの受け渡し、および単一の継承で十分な場合、ジェネリック関数は一歩後退したように見えるかもしれません。この理由は、前述のように、すべての種類の類似した名前付き機能を 1 つの汎用関数に入れたくないためです。

いつ

(defmethod bark ((some-animal dog)) ...)
(defmethod bark ((some-tree oak)) ...)

似ているように見えますが、これらは概念的に異なる 2 つのアクションです。

しかし、より多くの:

(defmethod bark ((some-animal dog) tone loudness duration)
   ...)

(defmethod bark ((some-tree oak)) ...)

同じ名前のジェネリック関数のパラメーター リストが突然異なって見えます。それを1つの汎用関数にすることを許可する必要がありますか? BARKそうでない場合、適切なパラメーターを使用して、もののリスト内のさまざまなオブジェクトをどのように呼び出すのでしょうか?

実際の Lisp コードでは、ジェネリック関数は通常、いくつかの必須およびオプションの引数を使用して、はるかに複雑に見えます。

Common Lisp では、ジェネリック関数もメソッド型が 1 つだけではありません。方法にはさまざまな種類があり、それらを組み合わせる方法もさまざまです。それらが実際に特定のジェネリック関数に属している場合にのみ、それらを組み合わせることに意味があります。

ジェネリック関数もファースト クラス オブジェクトであるため、それらを渡したり、関数から返したり、データ構造に格納したりできます。この時点で、名前ではなく汎用関数オブジェクト自体が重要になります。

x 座標と y 座標を持ち、ポイントとして機能するオブジェクトがある単純なケースでは、クラスからオブジェクトのクラスを継承しますPOINT(おそらく mixin として)。GET-X次に、必要に応じて、GET-Yシンボルを名前空間にインポートします。

Lisp/CLOS とはさらに異なり、マルチメソッドをサポートしようとする言語が他にもあります。

それをJavaに追加する試みがたくさんあるようです。

于 2012-03-04T08:35:44.517 に答える
9

「マルチメソッドが機能しない理由」の例では、同じ言語名前空間で同じ名前の2つのジェネリック関数を定義できると想定しています。これは一般的には当てはまりません。たとえば、Clojure のマルチメソッドは名前空間に明示的に属しているため、同じ名前の汎用関数が 2 つある場合は、どちらを使用しているかを明確にする必要があります。

つまり、「概念的に異なる」関数は、常に異なる名前を持つか、異なる名前空間に存在します。

于 2012-03-04T08:29:19.183 に答える
3

ジェネリック関数は、そのメソッドが実装されているすべてのクラスに対して同じ「動詞」を実行する必要があります。

動物/木の「樹皮」の場合、アニマル動詞は「サウンド アクションを実行する」であり、木の場合は、まあ、make-environment-shield だと思います。

英語がたまたま両方を「樹皮」と呼んでいるのは、言語学的な偶然にすぎません。

複数の異なる GF (ジェネリック関数) が本当に同じ名前を持つ必要がある場合は、名前空間を使用してそれらを分離するのが (おそらく) 正しいことです。

于 2012-03-04T11:45:14.347 に答える
2

XYZ の get-x() は GENE の get-x() と概念的な関係がないため、これらは別のジェネリック関数として実装されています。

もちろん。しかし、それらの arglist は同じ (オブジェクトをメソッドに渡すだけ) であるため、それらを同じジェネリック関数の異なるメソッドとして実装することができます。

メソッドをジェネリック関数に追加するときの唯一の制約は、メソッドの arglist がジェネリック関数の arglist と一致することです。

より一般的には、メソッドは必須パラメーターとオプション パラメーターの数を同じにする必要があり、ジェネリック関数によって指定された &rest または &key パラメーターに対応する引数を受け入れることができなければなりません。

関数が概念的に関連していなければならないという制約はありません。ほとんどの場合 (スーパークラスのオーバーライドなど) ですが、そうである必要はありません。

この制約 (同じ arglist が必要) でさえ、制限があるように見えることがあります。Erlang を見ると、関数にはアリティがあり、同じ名前でアリティが異なる複数の関数 (同じ名前で arglist が異なる関数) を定義できます。そして、一種のディスパッチが適切な関数を呼び出します。私はこれが好き。Lisp では、これはジェネリック関数がさまざまな arglist を持つメソッドを受け入れるようにすることに対応すると思います。たぶん、これは MOP で設定可能なものでしょうか?

hereをもう少し読んでみると、キーワード引数を使用すると、プログラマーは、異なるメソッドで異なるキーを使用して引数の数を変えることで、ジェネリック関数に完全に異なるアリティを持つメソッドをカプセル化させることができるように思われます。

メソッドは、&rest パラメーターを持つか、同じ &key パラメーターを持つか、&key と共に &allow-other-keys を指定することにより、そのジェネリック関数で定義された &key および &rest 引数を「受け入れる」ことができます。メソッドは、ジェネリック関数のパラメーター リストにない &key パラメーターを指定することもできます。ジェネリック関数が呼び出されると、ジェネリック関数または適用可能なメソッドによって指定された &key パラメーターが受け入れられます。

また、ジェネリック関数に格納されたさまざまなメソッドが概念的に異なることを行うこの種のぼかしは、「木の樹皮」、「犬の樹皮」の例の舞台裏で発生することに注意してください。ツリー クラスを定義するときは、bark スロットの自動ゲッター メソッドとセッター メソッドを設定します。犬のクラスを定義するときは、実際に吠える犬の種類に対して bark メソッドを定義します。そして、これらのメソッドは両方とも #'bark ジェネリック関数に格納されます。

どちらも同じジェネリック関数に含まれているため、まったく同じ方法で呼び出すことができます。

(bark tree-obj) -> Returns a noun (the bark of the tree)
(bark dog-obj) -> Produces a verb (the dog barks)

コードとして:

CL-USER> 
(defclass tree ()
  ((bark :accessor bark :initarg :bark :initform 'cracked)))
#<STANDARD-CLASS TREE>
CL-USER> 
(symbol-function 'bark)
#<STANDARD-GENERIC-FUNCTION BARK (1)>
CL-USER> 
(defclass dog ()
  ())
#<STANDARD-CLASS DOG>
CL-USER> 
(defmethod bark ((obj dog))
  'rough)
#<STANDARD-METHOD BARK (DOG) {1005494691}>
CL-USER> 
(symbol-function 'bark)
#<STANDARD-GENERIC-FUNCTION BARK (2)>
CL-USER> 
(bark (make-instance 'tree))
CRACKED
CL-USER> 
(bark (make-instance 'dog))
ROUGH
CL-USER> 

私は、この種の「構文の二重性」や機能のぼやけなどを好む傾向があります。また、ジェネリック関数のすべてのメソッドが概念的に似ている必要はないと思います。それは単なるガイドラインIMOです。英語で言語的な相互作用 (名詞と動詞としての吠え声) が発生した場合、大文字と小文字を適切に処理するプログラミング言語があると便利です。

于 2012-03-04T17:41:39.210 に答える
2

メッセージパッシングOOは、一般に、あなたが話している名前空間の問題を解決しません。構造型システムを持つオブジェクト指向言語では、型が同じである限りbark、 an 内のメソッドと a 内のメソッドを区別しません。そのように見えるのは、一般的な OO 言語が公称型システム (Java など) を使用しているからです。AnimalTree

于 2012-03-04T17:21:46.423 に答える
1

名前空間、グローバルジェネリック関数、ローカルジェネリック関数 (メソッド)、メソッド呼び出し、メッセージパッシングなど、いくつかの概念を使用してそれらを混合しています。

状況によっては、これらの概念が構文的に重複する場合があり、実装が困難でした。あなたも頭の中でたくさんの概念を混ぜ合わせているように思えます。

関数型言語は私の強みではありません。LISP でいくつかの作業を行いました。

ただし、この概念の一部は、手続き型やオブジェクト (クラス) 指向など、他のパラダイムで使用されています。この概念がどのように実装されているかを確認し、後で独自のプログラミング言語に戻ることができます。

たとえば、手続き型プログラミングとは別の概念として名前空間(「モジュール」)を使用し、識別子の衝突を避けることが非常に重要だと考えています。あなたのような名前空間を持つプログラミング言語は、次のようになります。

=== ファイル animal.code ===

define module animals

define class animal
  // methods doesn't use "bark(animal AANIMAL)"
  define method bark()
  ...
  end define method
end define class

define class dog
  // methods doesn't use "bark(dog ADOG)"
  define method bark()
  ...
  end define method
end define class

end define module

=== ファイル内 myprogram.code ===

define module myprogram

import animals.code
import trees.code

define function main
  a = new-dog()
  a.bark() //Make the dog bark

  …

  t = new-tree()
  b = t.bark() //Retrieve the bark from the tree
end define function main

end define module

乾杯。

于 2012-04-30T23:16:16.113 に答える