ラベル JavaScript の投稿を表示しています。 すべての投稿を表示
ラベル JavaScript の投稿を表示しています。 すべての投稿を表示

2010-07-03

関数とメソッドの違い

オブジェクト指向言語におけるメソッドを関数で実装できるだろうか?

最近のオブジェクト指向言語では,関数型言語の特徴であるファーストクラスオブジェクトとしての関数が提供されていることが多い.このような言語の場合、メソッドは関数なのだろうか?インスタンス変数に関数を代入すればそれが即ちメソッドなのだろうか?しかしことはそう単 純ではない.レシーバ (C++, Java, JavaScript における this) の扱いをどうするかという問題があるからである.

JavaScript

JavaScript ではまさにメソッドが関数として実装されている.

o = {
    data: "hello",
    method: function () { alert(this.data); }
};
o.method();

とすれば "hello" が表示される.それでは以下のように一旦関数を単離するとどうなるだろうか?関数がファーストクラスオブジェクトであるからには単離して持ち運ぶことが可能でなければならない.

m = o.method;
m()

この場合 undefined が表示される.いったい method 中の this は何を指しているのであろうか?引き続いて

data = "world";
m()

を実行してみれば分かる通り ("world" が表示される),この場合の this はグローバルオブジェクト (window) を指している.次に全然別のオブジェクトに持って行ってみよう.

oo = {
    data: "!",
    method: o.method
};
oo.method();

今度は "!" が表示される.つまり JavaScript では呼び出し方に応じて this が指すものを変えることでメソッドを関数として実装することを可能にしていると言える.

JavaScript の仕様書では,object.variable は単純な値へと評価されるのではなく,(object の値, "variable") という組へと評価されると説明されている (8.7 The Reference Type). この組に対して関数呼び出しが適用される (object.variable()) と,this が「object の値」を指すように設定されて,object["variable"] というプロパティ値の関数が呼び出されることになる (11.2.3 Function Call). この仕様を見れば上記の挙動も理解できるだろう(「object の値」が無いときには this はグローバルオブジェクトを指す,という記述が仕様書にある).

ちなみにこの Reference Type はカッコ式を素通りするので,

(o.method)()

のときも thiso を指し,"hello" が表示される.ただしコンマ演算子が適用されるとプロパティ値へと変換されるので,

(1, o.method)()

のときの this はグローバルオブジェクトを指し,"world" が表示される.かなり高度な JavaScript パズル問題といえるだろう.

Python

Python では,メソッドのレシーバをプログラマが明示的に宣言しなければならない点で JavaScript よりもさらに単純にメソッド=関数となっているように見える.

class Foo(object):
    def __init__(self):
        self.data = "hello"
    def method(self):
        print self.data
foo = Foo()
foo.method()

とすれば当然 "hello" が表示されるが,以下はどうだろうか?

m = foo.method
m()

class Bar(object):
    def __init__(self):
        self.data = "world"
    method = m
bar = Bar()
bar.method()

いずれも "hello" が表示される.Python では JavaScript のように () による関数呼び出しの時点でレシーバが決まるのではなく,インスタンス変数の内容を取得する時点でレシーバが決定されるようになっている.

以下ではわかりやすいように,メソッド関数をグローバルで定義して実験してみる.

def m(self):
    print self.data

class Foo(object):
    def __init__(self):
        self.data = "hello"
    method = m

class Bar(object):
    def __init__(self):
        self.data = "world"
    method = m

foo = Foo()
bar = Bar()
print foo.method == m
print bar.method == m
print foo.method == bar.method

この場合いずれも False が表示される.このように,同一の関数をインスタンス変数に設定したにもかかわらず,インスタンス変数の内容を取得して比較すると異なっていることが分かる.

実は Python ではインスタンス変数へのアクセスの際に,それがユーザ定義関数であれば,ユーザ定義メソッドという別のオブジェクトが返される (Python リファレンス「3.2 標準型の階層」の「ユーザ定義メソッド」).このオブジェクト (o とする) には o.im_func に元の関数が,o.im_self にレシーバがセットされ,メソッド呼び出し時の関数とレシーバとして用いられる.

Python のメソッド=関数かつレシーバ引数の明示という仕様はシンプルできれいかもしれないが,そのしわよせは別の場所,しかも仕様書のどこにあるかもよくわからないような所に隠されている.

まとめ

関数でメソッドを実現することは一見単純なことのように思えるが,実際はそうでもないということがわかる.

Perl や Ruby でどうなっているかを調べてみるのもおもしろいだろう.

2007-11-13

JavaScript におけるオブジェクト

メソッド呼び出し o.f() を考えてみる.JavaScript ではメソッドはオブジェクトのプロパティで型が関数であるものを言う.ただし,メソッドは言語仕様としては存在しない(追記参照).単に外見がそう見えるというだけの話である.

オブジェクト指向を実装した言語を考える場合に,メソッド呼び出しと継承との関係が非常に重要である.この関係が存在しないオブジェクト指向言語はありえない.JavaScript のメソッドはプロパティであるのだから,オブジェクトからプロパティを取得するところに JavaScript のオブジェクト指向サポートの本質が隠れているに違いない.そこで仕様書からプロパティの取得部分を引用する.以下で [[XXX]] は内部(隠し)プロパティであることを示す.

8.6.2.1 [[Get]] (P)

O の [[Get]] メソッドがプロパティ名 P で呼出されると,次のステップがとられる:

  1. O が P という名前のプロパティを持っていなければ,ステップ 4 へ進む.
  2. そのプロパティの値を取得する.
  3. Result(2) を返す.
  4. O の [[Prototype]] が null ならば,undefined を返す.
  5. [[Prototype]] の [[Get]] メソッドを,プロパティ名 P で呼び出す.
  6. Result(5) を返す.

この仕様から何が言えるだろうか.

  • オブジェクト指向におけるインスタンスが作成できる.各オブジェクトに個別にメソッドをもたなくても,オブジェクトを 1 つ用意してメソッドを定義して各オブジェクトの [[Prototype]] に指させることで共通のメソッド群を持つ複数のオブジェクト,つまりインスタンスを作成できる.
  • オブジェクト指向における継承を実現できる.ステップ 5 で発生する再帰によって,複数のメソッド群の線形探索が可能になっている.また,線形探索によりメソッドのオーバーライドが実現されている.

ではオブジェクトの [[Prototype]] を設定するにはどうしたらよいのだろうか.o.prototype = { ... } としてしまいそうだが,これではだめである.内部 [[Prototype]] プロパティは .prototype ではない.内部 [[Prototype]] プロパティを設定するには new 式を用いる.

o = new O() としたいが,O は何だろうか.仕様によると,O は内部 [[Construct]] プロパティを持つオブジェクトでなければならない(11.2.2 new 演算子).内部 [[Construct]] プロパティを持つオブジェクトとは JavaScript では関数オブジェクトのみである.いくつかの標準オブジェクト(Object, Array など)も関数オブジェクトである.このような関数オブジェクトはコンストラクタと呼ばれる.

o = new O() とすると,内部 [[Construct]] プロパティ(メソッド)が呼ばれる(11.2.2 new 演算子).内部 [[Construct]] メソッドでオブジェクトが生成され,生成されたオブジェクトの内部 [[Prototype]] プロパティに O.prototype の値が設定される(13.2.2 [[Construct]]).

O.prototype に設定するプロパティは,メソッドのように各オブジェクト(インスタンス)に共有されるものであることに注意する必要がある(数,文字列は結果的に共有さ れない—immutable なので変更しようとすると代入することになり,差し替わる).O.prototype にインスタンス変数を定義してはいけない(クラス変数になってしまう).o = new O() とすると,オブジェクトが生成されたあとに,そのオブジェクトを this として関数 O() が呼び出される.つまり O はコンストラクタとして機能する.このコンストラクタの中で作成したプロパティがインスタンス変数となる.

こうして JavaScript ではオブジェクトシステムが実現されている.

(2012/5/31 追記) 「メソッドは言語仕様としては存在しない」と書いたが、そんなこともない。o.f() により o.f にセットされた関数を呼び出すと、o が隠れた引数として設定され、呼ばれた側から this により参照できる。ちなみにメソッド呼び出しで無い関数呼び出し g() では、this はグローバルオブジェクト(window)を指す。