Rubyのyieldは羊の皮を被ったevalだ!
Yugui著「初めてのRuby」の9章に、Rubyの黒魔術の一つとしてeval族と称されるメソッド群が紹介されている。
危険らしい。素人が安易に手を出すべきではなさそうだ。でも魅力的らしい。
暗黒の世界に引かれていく自分がいる…
勉学のために覗くだけならいいだろうし、危険であればその正しい理解がより重要になるだろう。自分が学んで理解したことをここに整理してみよう。
eval族と呼ばれるものには、instance_evalメソッド、class_evalメソッド(またはmodule_eval)、および組み込み関数evalがある。
instance_eval
Ruby空間における操作対象はオブジェクトである。オブジェクトは外からのメッセージを受け取ると、その中の対応するメソッドを起動して、そこに書かれている手続きを実行する。
メソッドは他のオブジェクトを引数として取ることができる。引数として渡されたオブジェクトは、メソッドにおいてオブジェクトとして操作され、メソッド内の他のオブジェクトと協同して、その処理の結果をメッセージ送信者に返す。
instance_evalメソッドは文字列オブジェクトを引数として取ることができる。しかし、このメソッドは他の一般的なメソッドとは異なり、これを文字列オブジェクトとしては扱わない。これをRubyの手続きとして扱う。
[1,2,3].instance_eval "print 'Hello'" # => Hello
ここでprintは、トップレベルで実行されているように見えるけれども、”print ‘Hello’“はinstance_evalの引数として、配列オブジェクト[1,2,3]に渡されているから、この配列オブジェクト内で実行されている、ということを理解しなければならない。
そのことはこうするとよく分かるかもしれない。
[1,2,3].instance_eval "print reverse" # => [3,2,1]
文字列中のreverseは明らかに、メッセージを受け取る配列オブジェクトに適用されている。
つまりinstance_evalはオブジェクトの外にいてオブジェクトの中のコンテキストで、渡された文字列をRubyコードとして評価する。
これは確かに恐ろしいことかもしれない。なぜなら完成したプログラムに対して、そのユーザが後からキーボードで文字列を入力することによりコードを追加し改変し、場合によっては破壊できることを意味するからだ。
class Account
@@bank_money = 0
def initialize(balance)
@balance = balance
@@bank_money += @balance
end
end
my_account = Account.new(10000)
my_account.instance_eval "print @balance = @balance * 100"
# =>1000000
インスタンス変数@balanceは、my_accountオブジェクトの内部状態を保持する。通常この値にアクセスするには、クラスにそのアクセッサメソッドを用意し、これを介さなければならない。
def balance
@balance
end
def balance=(amt)
@balance = amt
end
または
attr_accessor :balance
Rubyのようなオブジェクト指向言語ではオブジェクトへのアクセスは原則、用意されたメソッドからしか行えない。これがオブジェクトを予期せぬ変更や破壊から守る。
しかしinstance_evalを使えば上のようにアクセッサメソッドを介さずに、インスタンス変数@balanceの参照を変更し、その結果にアクセスすることが可能となる。
instance_evalを使えばメソッド定義も難なくできてしまう。
my_account = Account.new(10000)
his_account = Account.new(30000)
my_account.instance_eval "def transfer_all_to_me; @balance += @@bank_money; end"
#メソッドを定義する
my_account.transfer_all_to_me
my_account.instance_eval "print @balance"
# => 50000
transfer_all_to_meは 僕のアカウントオブジェクトのコンテキストで生成されるので1、僕のオブジェクト専用のメソッド、つまりSingletonメソッド(抽象メソッド)だ。これで誰かが貯金をするたびに僕はお金持ちになっていく!
ここでは示されていないけれども、instance_evalを使えば、ユーザから受け取った文字列を名前としてメソッドを動的に定義する、という荒技も可能だ
行指向の文字列にはヒアドキュメントを使った方が見栄えがいい。
my_account.instance_eval <<DEF
def transfer_all_to_me
@balance += @@bank_money
end
DEF
こうすると、まるでブロックを渡しているように見える。
期待に違わず、instance_evalはブロックも受け取る。ブロックの中身を受け取ったオブジェクトのコンテキストでRubyコードとして評価する。だから上のコードはこうも書ける。
my_account.instance_eval do
def transfer_me_all
@balance += @@bank_money
end
end
当然にブロックには引数を渡したくなる。それが人情というものだ。Ruby1.9ではinstance_execがそれを可能にする。
my_account.instance_exec(2) do |arg|
@@bank_money *= arg
def transfer_me_all_with_double
@balance += @@bank_money
end
end
my_account.transfer_me_all_with_double
my_account.instance_eval "print @balance"
# => 90000
僕の口座が倍になった!
class_eval(module_eval)
でもあまりにこれじゃ不公平だ。僕にだって幾らかの良心というものがある。そう思ったらclass_evalを使おう。
class_evalは、そのクラスのコンテキストで文字列やブロックを評価する。だからブロックでメソッド定義をすれば、そのメソッドはクラスのインスタンスメソッドになる。
my_account = Account.new(10000)
his_account = Account.new(30000)
Account.class_eval do
def transfer_all_to_me
@balance += @@bank_money
end
end
my_account.transfer_all_to_me
his_account.transfer_all_to_me
my_account.instance_eval "print @balance" # => 50000
his_account.instance_eval "print @balance" # => 70000
これでみんながハッピーになれる!
module_evalはclass_evalと同様に、そのモジュールのコンテキストで文字列やブロックを評価する。これを使えば後からモジュールにクラスを定義するようなことができる。
eval
Rubyにはオブジェクトを意識しないで使える、evalも用意されている。
eval "print 'Hello'" # => Hello
evalはRubyの組み込み関数、つまりObjectクラスのインスタンスメソッドだ。これはトップレベルオブジェクトmainのinstance_evalと等価になる。
eval "print self" # => main
self.instance_eval "print self" # => main
つまりデフォルトでevalはmainオブジェクトのコンテキストで文字列を評価する。
でも、第2引数に他のコンテキストを持ったBindingオブジェクトを与えた場合、evalはそのコンテキストで文字列を評価する。これによりevalの実行コンテキストをトップレベル以外にすることができる。
class Account
@@bank_money = 0
def initialize(balance)
@balance = balance
@@bank_money += @balance
end
def bind # accountの環境情報を返すメソッド
binding
end
end
my_account = Account.new(10000)
# evalの第2引数にBindingオブジェクトを渡す
# ヒアドキュメントはこういうときでも便利に使える
eval(<<DEF, my_account.bind)
def transfer_all_to_me
@balance += @@bank_money
end
DEF
my_account.transfer_all_to_me
my_account.instance_eval "print @balance"
# => 20000
これは先の例のinstance_evalの使い方と等価である。evalの第2引数にmy_accountオブジェクトのコンテキストを渡すことにより、そのコンテキストでブロックを評価する。
evalはinstance_evalやclass_evalと異なり、引数にブロックを取れない。
yield
結局eval族は、その引数に与えられた文字列またはブロックを、それが置かれたコンテキストとは別のコンテキストで評価できるようにするものだ。
Rubyにおいてブロックを評価する一般的方法は、ブロックを渡すメソッド内でyieldを呼ぶことである。
class String
def speak
yield
end
end
"Charlie".speak { print "hello" } # => hello
ブロックが評価されるコンテキストは基本的にそれが置かれたコンテキストだけれど、yieldに引数を取ることによってこれを変えることができる。
class String
def speak
yield
end
def talk
yield self # selfを引数に取る
end
end
"Charlie".speak { print self } # => main
"Charlie".talk { |this| print this } # => Charlie
ブロックには当然メソッド定義を置くこともできるので、コンテキストの切替えと共にこれを用いれば、先に示したAccountクラスのインスタンス変数にもアクセスできるようになる。
class Account
def initialize(balance)
@balance = balance
end
def yield_eval #ブロックを評価するための汎用メソッド
yield self
end
end
my_account = Account.new(10000)
my_account.yield_eval do |this|
def this.add_money(i)
@balance += i
end
end
p my_account.add_money(10000) # => 20000
ここでadd_moneyメソッドは、my_accountオブジェクトのSingletonメソッドである。だからブロックを受ける汎用メソッドを予め用意しておけば、先の例のinstance_eval相当のことができるようになる2。
class_eval相当の処理をyieldで実現することもできる。
my_account = Account.new(10000)
his_account = Account.new(30000)
my_account.yield_eval do
def add_money(i)
@balance += i
end
public :add_money
end
p my_account.add_money(10000) # => 20000
p his_account.add_money(10000) # => 40000
ここでyield_evalメソッドはAccountクラスにadd_moneyメソッドを追加する。これでみんながハッピーになれる!
こうしてみるとyieldは、evalの底知れないパワーには及ばないとしても、プログラミングに高い自由度を与える強力なツールであることは間違いないし、まだまだ秘められたパワーを持っていそうだ。そう、だからRubyのyieldは…
羊の皮を被ったevalに違いない!
関連記事: Rubyのシンボルは文字列の皮を被った整数だ! Rubyのブロックはメソッドに対するメソッドのMix-inだ! Rubyのクラスはオブジェクトの母、モジュールはベビーシッター
blog comments powered by Disqus