hp12c このページをアンテナに追加 RSSフィード

2008-08-12

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のシンボルは文字列の皮を被った整数だ! - hp12c

Rubyのブロックはメソッドに対するメソッドのMix-inだ! - hp12c

Rubyのクラスはオブジェクトの母、モジュールはベビーシッター - hp12c

*1:Ruby1.9。Ruby1.8.7ではクラス変数@@bank_moneyがトップレベルのコンテキストで評価されてしまいうまく動作しません。なぜだろう?

*2:先の例におけるクラス変数@@bank_moneyのコンテキストは、yieldを用いる場合なぜかmainのまま変わりません。理由は分かりません。その対応版の作成は断念しました。