Scala覚え書き その2 thisの謎
オブジェクト指向な言語で、かつ関数がファーストクラスオブジェクトな言語だと、いわゆるthisとインスタンスメソッドの関係が個人的には超気になる。
オブジェクト指向な言語だと、インスタンスメソッドの中から(thisとかselfとかいう名前で)属するオブジェクトそのものを取得できる。さらに関数がファーストクラスオブジェクトだと、あるオブジェクトのインスタンスメソッドを「値として取り出して」、他のオブジェクトのスロットだかフィールドだかに代入することで、インスタンスメソッドをすげ換えることができる。
個人的に超気になっているポイントは、いったんインスタンスメソッドとして定義された関数の中に「this」を使った式が書かれている場合、その関数を他のオブジェクトのインスタンスメソッドに代入した時に「this」が何を指すか?というところ。おもしろいことに、そのような場合にthisが何を指すのかを決定する仕組みは言語によってかなり異なっている。
例えば、Javascriptだとthisの値が何を指すかは"その関数の呼ばれ方"に依存して決定されるし*1、Pythonだと関数をインスタンスメソッドとして呼びだした時は、第一引数(self)にオブジェクトそのものが渡されるという仕組みになっている*2。Rubyだとインスタンスメソッドから関数オブジェクトを取りだすことはできるものの、他のオブジェクトへの代入(bind)は制約が多くて*3あまり行われない。
で、Scalaはというと、以下のような実験をやってみた限りでは、他のオブジェクトにインスタンスメソッドを代入してもthisが指すオブジェクトは変化しない。JavascriptやPythonが上記の仕組みでthisが指すものを変化させているのとは対称的で、ちょっと驚いた。Scalaのthisのセマンティクスをちゃんと調べたわけではないけど、インスタンスメソッド中のthisはすげかわったりはしないのかな。たんなるクロージャー?
(追記:以下の実験は無名関数を値として保持するように書いてるけど、defで定義した時は挙動が違うのかもしれない。継承うんぬんはdefで定義したものにしか動作しない、っていう可能性はありそうだ)
こうなってると継承した時に意味的にややこしいことになる気がするんだけど、そういえばScalaの継承の仕組みがどうなっているかはまださっぱり調べてないのであった。Traitsっていうキーワードでmixin用のデータ(RubyのModuleみたいなもの)が定義できるらしいのだけど、その中に"this"が書いてあるとどうなるんだろう。Trait中では特別扱いされるのかな?
以下実験の様子。実行結果はWindows上のscala対話環境(version 2.6.1-final)でのもの。コメントに出力結果を書いといた。
class Test1 { var a = 1 var get_a = () => this.a } class Test2 { var a = 2 var get_a = () => this.a } val o1 = new Test1 val o2 = new Test2 println(o1.get_a()) // 1 println(o2.get_a()) // 2 o2.get_a = o1.get_a // o2のインスタンスメソッドをo1にすげかえると… println(o1.get_a()) // こちらは変わらず1 println(o2.get_a()) // これが1に! println(o2.a) // 直接参照してやると2のまま o1.a = 100 // o1のaの値を変更すると… println(o1.get_a()) // あたりまえだけど、これは100がでる println(o2.get_a()) // そしてあたりまえのような顔をしてこれも100 println(o2.a) // やっぱりこれは2のまま
*1:prototype.jsの中のBindはこの仕組みが引き起す非直感的な動作をフォローする仕組み、なはず
*2:そのせいで、インスタンスメソッドとして呼びだした時と、単なる関数として呼びだした時では引数の数が一個ずれる
*3:代入(bind)可能なのは、元のオブジェクトの情報が切りはなされたUnboundMethodという特殊な関数オブジェクトだけで、かつ代入(bind)可能なオブジェクトも同じクラス相当のオブジェクトに制約されている