前回は各オブジェクトの基本的な特徴を見ただけで終わってしまった。今回はこれらをコンテキストという観点から見てみたい。
前回のまとめ
| 呼び出し | 外側のscope | block | 中身 | 戻り値 | |
|---|---|---|---|---|---|
| __send__ | __send__ | 不可能(そもそもコンテキストを保存していない) | 可能 | 保持しない | メソッドの戻り値 |
| Method | [],call | 参照不可能 | 可能 | メソッド本体とself | メソッドの戻り値 |
| UnboundMethod | 不能 | 参照不可能 | - | 本体 | メソッドの戻り値 |
| Proc | [],call,yield | 参照可能 | 不可能 | closure | Procの最後の値 |
| Continuation | [],call | - | 不可能 | 「続き」 | 戻らない |
Proc#callにおいてブロック付きの呼び出しが不可能であることは前回は記述しなかった。 sshiさんにご指摘いただいた。
Procを作成するときに指定するブロック仮引数の記述は、メソッド定義の際の仮引数の記述にとても似ている。
adder = Proc.new{|a, b| a + b} # 普通の引数
sum = Proc.new{|*elements| elements.inject(0, sum){|elem| sum + elem} }
# *をつければ任意個の引数を配列として受け取れる
ならば、&を付ければブロック引数を受け取れそうなものだ。けれども、実際にはこれは構文エラーとなる。
Proc.new{|a, b, &block| :ok }
# => parse error, unexpected tAMPER
Procを作るにはブロックが必要だけれどもそもそもブロック構文そのものがブロックを受け付けるようにできていない。ブロック付き呼び出しに更にブロックを付けるというのは変だから普通は困らないのだけれど。
何かポリシーがあってブロックを取るProcの可能性を弾いているのかどうかは分からない。2006-11-20追記: まつもとさんからご指摘いただきました。Ruby 1.9ではProc#callはブロックをとれるそうです。
というあたりで、今回の本題に入る。
相互変換
メソッド、Methodオブジェクト、UnboundMethodオブジェクト、ブロック、Procオブジェクト、Continuationオブジェクトは相互に変換が可能である。ただし、変換といってもそれぞれに特徴があるオブジェクトだから、完全に変換先になりきってしまうわけではない。もっとも、なりきってしまったら面白くない。変換の結果としてブロックのようなメソッドやMethodオブジェクトのようなProcが生まれるから味があるのである。
メソッドの作成
メソッドは普通はdef文で作るのであった。Rubyにはもう1つメソッドを作る方法がある。Module#define_methodである。
define_method(name, method)
define_method(name) { ... }
このメソッドは第2引数にMethod, UnboundMethod, Procのいずれかを渡す、もしくはブロック付きで呼び出して使う。これによりMethod, UnboundMethod, Proc, ブロックをメソッドに変換できる訳だ。
もっとも、Method, UnboundMethodのほうはあまり便利とは思わない。前回触れたMethodとUnboundの変換と同様で、異なるクラスにはメソッドを移せないからだ。*1
ブロック付きで呼び出した場合はブロックはProcオブジェクトに変換されてから処理される。だから、次の2つは同じことだ。
define_method(:foo) do |a, b, c|
p [a, b, c]
end
define_method(:foo, lambda{|a,b,c| p [a, b, c] })
ここで面白いのはProc(あるいはブロック)を渡した場合にはそれらが持っている外部スコープへの参照がそのまま生きるということ。これを用いることで、クラス定義文のローカル変数を参照するようなメソッドを定義できる。
class Hoge
local = 1
define_method(:foo) do
p local += 1
end
define_method(:bar) do
p local += 1
end
end
私がブロック付きのdefine_methodを用いるのはもっぱらこの形だ。def文と違って外側の変数スコープを参照できるので何かと便利だし、こうして捕らえたローカル変数への参照は、一度クラス定義文を抜けてしまったら外部からは参照できないのでサブクラスの実装と名前が衝突する心配がない。
クラス文の中からは外のスコープを参照できないので次のように書くとNameErrorを生じる
local = 1
class Hoge
define_method(:foo) do
local
end
end
hoge = Hoge.new
hoge.foo # => NameError: undefined local variable or method `local' for #<Hoge:0x6e28>
けれども、class_evalを用いれば外側を参照可能である。
class Hoge; end
local = 1
Hoge.class_eval do
define_method(:foo) do
local
end
end
このやりかたは前回定義したsingleton_class_evalと組み合わせるとメソッド内で特異クラスを生成する際に便利だ。テスト用に簡易なMockを作成するなどの際に重宝する。
def test_foo
received_args = [] # ここに渡された実引数の値がたまる
mock = Object.new
mock.singleton_class_eval do
define_method(:callback) do |*args|
received_args << args
end
end
testee(mock) # テストしたいメソッド
# これがmock.callbackを呼ぶはず
assert_xxx ...
end
Methodオブジェクトの作成
Methodオブジェクトは前回述べたように、メソッドを元にしてObject#methodで作成するのであった。このほか、UnboundMethod#bindでも作成できる。
UnboundMethodの作成
UnboundMethodはメソッドを元にしてModule#instance_methodを用いるか、Method#unbindを用いるのであった。
ブロックの作成
ブロックは操作の対象と言うよりは構文の形だから、それ自体をプログラム上で作成する方法というのはない。けれども、ブロック付き呼び出しの代わりにProcオブジェクトに&を付けて呼び出すことはできるのであった。次の2つはほぽ等価である。
collection.each do |item|
p item
end
proc = Proc.new{|item| p item}
collection.each(&proc)
Procオブジェクトの作成
Procオブジェクトはブロックを元にしてProc.newまたはKernel#lambdaで作るのが基本であった。
このほか、Methodはto_procメソッドを持っており、Procへの変換が可能である。
proc = hoge.method(:foo).to_proc
proc.is_a? Proc # => true
Continutationオブジェクトの作成
ContinuationはKernel#callccにブロックを渡すと作成できるのであった。が、これは渡したブロックを元に作成されたとは言えない。そのブロックはContinuationオブジェクトを受け取る受け皿として使われるのであり、Continuationオブジェクトの元になっているのは「callcc以降の処理」であった。
相互関係
以上を図にすると次のようになる。Continuationはやはり特殊なので除いてある。
コンテキスト
上の図を見て「メソッド -> Method -> Proc -> メソッド」のようなサイクルが存在することが分かると思う。ならばぐるぐるとサイクルを回してみたらどうなるかと思うのが人情というものだ。でも、ちょっと先にコンテキストの話をさせて欲しい。このサイクルはコンテキストという観点から見た方が楽しめる。
コンテキストとはおおざっぱに言えばローカル変数の状態、self, klassから成る。
ここでローカル変数というのは、参照できる限り外側のスコープも含めた全部だ。selfというのはデフォルトでメソッドを受け取る相手である。Rubyのプログラムにはいつでもselfがいて、擬似変数selfを通じてアクセスできるのであった。klassについては後述。今は忘れてくれて良い。
コンテキストというものを考えに入れてみると、各呼び出し可能オブジェクトは次のようなものであるといえる。
- Method
- メソッド定義の本体 + self
- UnboundMethod
- メソッド定義の本体
- Proc
- ブロックの本体 + コンテキスト
- Continuation
- 実行コード本体 + コールスタック + コンテキスト
ちなみに、コンテキストだけを表して本体のコードは持たないのがBindingである。C言語レベルではProcとBindingはどちらもstruct BLOCKを利用して実装されている。
Procがコンテキストを持っている証拠を見てみよう。
class Original
CONST = :original
$global = :original
@@class = :original
def create_proc
@instance = :original
local = :original
return Proc.new {
p caller
p self
p __FILE__
p __LINE__
p CONST
p $global
p @@class
p @instance
p local
}
end
def modify
@instance = :modified
local = :modfied
end
CONST = :modified
$global = :modified
@@class = :modified
end
class Other
CONST = :other
@@class = :other
$global = :other
def call(method)
local = :other
@instance = :other
method.call
end
end
orig = Original.new
proc = orig.create_proc
orig.modify # インスタンス変数を変更してしまう。
# 関係ないのが自明だけれども、ローカル変数も。
other = Other.new
other.call proc
これを実行すると次のようになる
| caller | ["proc.rb:42:in `call'", "proc.rb:52"] |
| self | #<Original:0x2466c @instance=:modified> |
| __FILE__ | "proc.rb" |
| __LINE__ | 14 |
| CONST | :modified |
| $global | :other |
| @@class | :modified |
| @instance | :modified |
| local | :original |
- 擬似変数self, __FILE__, __LINE__およびローカル変数はProc作成時のものがそのまま残っている。
- 定数、クラス変数、インスタンス変数は作成後の変更が反映されているが、探索はProc作成時の場所から行われている。呼び出し時のselfは関係ない
- グローバル変数は最後の変更が反映されている。そりゃそうだ。
ここから、定数やクラス変数、インスタンス変数、グローバル変数はコンテキストとして保存されるわけではないということも分かる。これらはselfを通じて間接的にアクセスされる。
同様にContinuationも見てみよう。
class Original
CONST = :original
$global = :original
@@class = :original
def create_cc
@instance = :original
local = :original
callcc{|c| return c}
p caller
p self
p __FILE__
p __LINE__
p CONST
p $global
p @@class
p @instance
p local
end
def modify
@instance = :modified
local = :modfied
def create_cc # メソッドも上書き
raise
end
end
CONST = :modified
$global = :modified
@@class = :modified
end
class Other
CONST = :other
@@class = :other
$global = :other
def call(cc)
local = :other
@instance = :other
cc.call
end
end
orig = Original.new
cc = orig.create_cc
orig.modify # インスタンス変数を変更してしまう。
# 関係ないのが自明だけれども、ローカル変数も。
if cc
other = Other.new
other.call(cc)
end
実行結果は次の通りである。
| caller | ["continuation.rb:50"] |
| self | #<Original:0x244b4 @instance=:modified> |
| __FILE__ | "continuation.rb" |
| __LINE__ | 14 |
| CONST | :modified |
| $global | :other |
| @@class | :modified |
| @instance | :modified |
| local | :original |
コンテキストが保存されているのは同じだが、callerの結果を見るとコールスタックも復元されていることが分かる。更に、メソッドの実装(本体)も書き換え前のものが使用されている。
まとめると、このようになる。
| ローカル変数 | self | 本体 | コールスタック | |
|---|---|---|---|---|
| __send__ | 参照不能 | 呼び出し時 | 呼び出し時 | 呼び出し時 |
| Method | 参照不能 | 作成時 | 作成時 | 呼び出し時 |
| UnboundMethod | 参照不能 | なし | 作成時 | 呼び出し時 |
| Proc | 作成時 | 作成時 | 作成時 | 呼び出し時 |
| Continuation | 作成時 | 作成時 | 作成時 | 作成時 |
コンテキストのすり替え
Rubyにはコンテキストをすり替えるメソッドがいくつかある。eval族と呼ばれる Kernel#eval, Object#instance_eval, Module#module_eval, Binding#evalである。
また、define_methodもコンテキスト差し替えメソッドの一種と見ることができる。
Kernel#eval
eval(expr[, binding[, fname[, lineno=1]]])
eval関数はeval族の代表格である。私はあまり使わないが。Perlと違って例外処理にevalを使わずに組み込みの例外機構を使うというのが大きいけれど。ちなみに、Rubyのevalはブロックを取れない。文字列をプログラムとして実行するのみである。
eval関数はデフォルトでは現在のコンテキストで文字列exprをRubyプログラムとしてコンパイルし、実行する。bindingを与えられるとbindingが内部に持っているコンテキストを利用する。Bindingクラスは主としてこの用途の為に設けられているクラスだ。
fnameやlinenoを与えるとファイル名や行番号情報をその値に差し替える。これらを見てみよう。
class Original
CONST = :original
$global = :original
@@class = :original
def create_binding
@instance = :original
local = :original
return binding # Kernel#binding
end
def modify
@instance = :modified
local = :modfied
end
CONST = :modified
$global = :modified
@@class = :modified
end
class Other
CONST = :other
@@class = :other
$global = :other
def call(program, binding)
local = :other
@instance = :other
eval(program)
puts
eval(program, binding)
puts
eval(program, binding, __FILE__)
puts
eval(program, binding, __FILE__, __LINE__)
end
end
orig = Original.new
binding = orig.create_binding
orig.modify # インスタンス変数を変更してしまう。
# 関係ないのが自明だけれども、ローカル変数も。
other = Other.new
other.call <<'EOS', binding
p caller
p self
p __FILE__
p __LINE__
p CONST
p $global
p @@class
p @instance
p local
EOS
これを実行すると次のようになった。
| 1 | 2 | 3 | 4 | |
|---|---|---|---|---|
| caller | ["eval.rb:51:in `eval'", "eval.rb:32:in `call'", "eval.rb:51"] | ["eval.rb:10:in `create_binding'", "eval.rb:46"] | ["eval.rb:10:in `create_binding'", "eval.rb:46"] | ["eval.rb:10:in `create_binding'", "eval.rb:46"] |
| self | #<Other:0x245cc @instance=:other> | #<Other:0x245cc @instance=:other> | #<Original:0x24644 @instance=:modified> | #<Original:0x24644 @instance=:modified> |
| __FILE__ | "(eval)" | "eval.rb" | "eval.rb" | "eval.rb" |
| __LINE__ | 4 | 13 | 4 | 44 |
| CONST | :other | :modified | :modified | :modified |
| $global | :other | :other | :other | :other |
| @@class | :other | :modified | :modified | :modified |
| @instance | :other | :modified | :modified | :modified |
| local | :other | :original | :original | :original |
ただし、1-4は順に
eval(expr)eval(expr, binding)eval(expr, binding, fname)eval(expr, binding, fname, lineno)
当たり前だが、文字列として書かれたプログラムはただの文字列であり一切のコンテキストを保持していない。そのため、eval実行時のコンテキストがそのまま反映されている。ただし、__FILE__は"(eval)"であり、__LINE__はeval文字列内の行番号である。
2番目を見るとBindingがコンテキストを保持しているのが分かると思う。binding作成時のコンテキストにすり替わっている。
3番目では、コンテキストはbindingに従うが、ファイル名だけが引数fnameに指定したものに変わっている。意外だったのは、__LINE__だ。bindingの持っている__LINE__ではなく、eval文字列内の行番号に戻ってしまっている。普通fnameを指定するなら一緒にlinenoも指定するから今まで気づかなかったが、言われてみればここで行番号だけbindingに従ってもかえって混乱するかもしれない。
4番目では、期待通りfnameとlinenoが反映されている。
fname, linenoはスタックトレース出力を親切にするためのものだ。例えばeval内でメソッドを定義したりしたとき、スタックトレースに"(eval)"の12行目とか書いてあっても該当するソースがどこにあるのか分からずに困ってしまう。そこで、eval実行時にfnameとlinenoを、渡した文字列リテラルの近くをポイントするように与えてやれば、例外発生時のヒントになる。
例えば、次はboolean属性のためのattr_accessorの変種である。文字列で与えられたメソッド内で例外が生じたとき、スタックトレースにevalの行が記されているのでとりあえずattr_booleanまではたどってくることができる。ここでは、BindingはKernel#bindingによってその場で作成している。
def attr_boolean(name)
eval <<-"EOS", binding, __FILE__, __LINE__
def #{name}?
@name != false
end
def #{name}=(value)
@name = value
end
EOS
end
Binding#eval
Binding#evalはRuby 1.9で導入された。機能的にはKernel#evalと変わらない。次の2つは等価である。
eval(expr, binding, fname, lineno)
binding.eval(expr, fname, lineno)
前記のattr_booleanのようにBindingをその場で作成するのではなく、予めどこかで作られているBindingオブジェクトを利用するときはBinding#evalのほうが自然な表記の気がする。
Object#instance_eval
Object#instance_evalは「selfの差し替え」に使う。このメソッドには文字列形式とブロック形式がある。
instance_eval(expr, [fname, [lineno=1]]) instance_eval {|obj| ... }
いずれにしても、与えられた文字列やブロックをinstance_evalのレシーバーのコンテキストで実行する。ただし、ブロックの場合、ローカル変数だけはもとのコンテキストのままだ。
次のプログラムを実行してみた。
class Original
CONST = :original
$global = :original
@@class = :original
end
class Original
def create_proc
@instance = :original
local = :original
return Proc.new{
p caller
p self
p __FILE__
p __LINE__
p CONST
p $global
p @@class
p @instance
p local
}
end
def modify
@instance = :modified
local = :modfied
end
const_set("CONST", :modified)
$global = :modified
@@class = :modified
end
class Other
CONST = :other
@@class = :other
$global = :other
def call(str, proc)
local = :other
@instance = :other
self.instance_eval(str)
puts
self.instance_eval(&proc)
end
end
orig = Original.new
proc = orig.create_proc
orig.modify # インスタンス変数を変更してしまう。
# 関係ないのが自明だけれども、ローカル変数も。
other = Other.new
other.call <<'EOS', proc
p caller
p self
p __FILE__
p __LINE__
p CONST
p $global
p @@class
p @instance
p local
EOS
結果はこうなる。
| 文字列版 | ブロック版 | |
|---|---|---|
| caller | ["instance_eval2.rb:56"] | ["instance_eval2.rb:46:in `call'", "instance_eval2.rb:56"] |
| self | #<Other:0x242d4 @instance=:other> | #<Other:0x242d4 @instance=:other> |
| __FILE__ | "(eval)" | "instance_eval.rb" |
| __LINE__ | 4 | 15 |
| CONST | :other | :modified |
| $global | :other | :other |
| @@class | :other | :modified |
| @instance | :other | :other |
| local | :other | :original |
文字列はやはりそれ自体はコンテキストを持っていないから全部otherのインスタンスのコンテキストになっている。この場合fnameやlinenoは渡していないので__FILE__や__LINE__はeval文字列のものだ。
Procを渡した場合が興味深い。ファイル名、行番号、ローカル変数を見るとコンテキストは保存されていることが分かるが、selfとそれから連動してインスタンス変数はotherのものに変わっている。一方、定数やクラス変数はOriginalのものを参照している。selfだけを差し替えるのだ。
instance_evalに渡したブロックを評価するときにはinstance_evalのレシーバーがselfに成っている、ということはprivateメソッドやインスタンス変数に外からアクセスできるし、しかも今のスコープのローカル変数は変わらずアクセスできるということだ。
class Foo
def initialize
@hidden = 1
end
private
def hidden
99
end
end
foo = Foo.new
local = 10
foo.instance_eval do
p @hidden + local
p hidden + local
end
けれども、こんなのはinstance_evalの一番つまらない使い方に過ぎない。selfの差し替えをうまく活かしているのがRuby/Tkである。
TkButton.new do
text 'OK'
command proc{exit}
pack
end
まるで設定ファイルでも呼んでいるかのようだ。はやりの言い方をするなら言語内DSLである。勿論、textとかcommandなどというメソッドがKernelに定義されているわけではない。これらはTkButtonオブジェクトのメソッドだ。Ruby/Tkはselfを差し替えてこのような記述を可能としているのである。
私はRakeこそこういうやり方をすればよかったのに、と思っている。 Rakeのタスク定義は次のようなのがお決まりの書き方だ。
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
この"t."は邪魔では無かろうか。私はこう書きたい。
Rake::TestTask.new(:test) do
libs << 'lib'
pattern 'test/**/*_test.rb'
verbose true
end
Rakeの初期構想段階ではどうやらこういう構文だったらしいけれど、進化の過程で何故か余計なブロック引数が付いてしまっている。確かにselfを差し替えるとブロックの外側ではアクセスできていたインスタンス変数やメソッドにアクセスできなくなるという弊害もあるけれども、普通はRakeのタスク定義をインスタンスメソッドで実行しないだろう。なら、普通のRakefileを書くぶんには下の書き方のほうが美しいではないか。
module_eval
module_evalはModuleのメソッドである。aliasとしてclass_evalというのも定義されている。モジュール相手にclass_evalと書いたりクラス相手にmodule_evalと書いたりするのが気持ち悪い人は使い分ければよい。
module_evalもselfを差し替える。
class Original
CONST = :original
$global = :original
@@class = :original
def create_proc
@instance = :original
local = :original
return Proc.new{
p caller
p self
p __FILE__
p __LINE__
p CONST
p $global
p @@class
p @instance
p local
}
end
def modify
@instance = :modified
local = :modfied
end
CONST = :modified
$global = :modified
@@class = :modified
end
class Other
CONST = :other
@@class = :other
$global = :other
@instance = :other_class
def Other.call(str, proc_by_method)
local = :other
@instance = :other
module_eval(str)
puts
module_eval(&proc_by_method)
end
end
orig = Original.new
proc_by_method = orig.create_proc
orig.modify # インスタンス変数を変更してしまう。
# 関係ないのが自明だけれども、ローカル変数も。
Other.call <<'EOS', proc_by_method
p caller
p self
p __FILE__
p __LINE__
p CONST
p $global
p @@class
p @instance
p local
EOS
これを実行したら次のようになった。
| 文字列版 | ブロック版 | |
|---|---|---|
| caller | ["module_eval2.rb:55"] | ["module_eval2.rb:46:in `call'", "module_eval2.rb:55"] |
| self | Other | Other |
| __FILE__ | "(eval)" | "module_eval.rb" |
| __LINE__ | 4 | 14 |
| CONST | :other | :modified |
| $global | :other | :other |
| @@class | :other | :modified |
| @instance | :other | :other |
| local | :other | :original |
instance_evalと変わらないではないか。ModuleだってObjectのサブクラスだろうに、わざわざinstance_evalとは別にmodule_evalを定義する意味はどこにあるのだろう。さぁ、klassを説明するときが来た。
予告
ちょっと長くなりすぎたのでここらで一回ポストする。次回はklassを探って、次回こそは呼び出し可能オブジェクトを相互変換を通じて弄くってみる。
*1厳密には、スーパークラスからサブクラスには移せる
コメント
1.9ではproc.callがブロックを受け付けます。
いままでProc.callがブロックを受け付けなかったのはおそらくブロックパラメータの部分の実現に多重代入が使われていたからだったと思います。
探ってみたらありました。
<a href="http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/13311">http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/13311</a>