Rubyの呼び出し可能オブジェクトの比較 (2) - というよりコンテキストの話

前回 は各オブジェクトの基本的な特徴を見ただけで終わってしまった。今回はこれらをコンテキストという観点から見てみたい。

前回のまとめ

呼び出し外側のscopeblock中身戻り値
__send____send__不可能(そもそもコンテキストを保存していない)可能保持しないメソッドの戻り値
Method[],call参照不可能可能メソッド本体とselfメソッドの戻り値
UnboundMethod不能参照不可能-本体メソッドの戻り値
Proc[],call,yield参照可能不可能closureProcの最後の値
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はやはり特殊なので除いてある。

ruby-convertion-20061119.png

コンテキスト

上の図を見て「メソッド -> 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

これを実行すると次のようになった。

1234
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__413444
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は順に

  1. eval(expr)

  2. eval(expr, binding)

  3. eval(expr, binding, fname)

  4. 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__415
CONST:other:modified
$global:other:other
@@class:other:modified
@instance:other:other
local:other:original

文字列はやはりそれ自体はコンテキストを持っていないから全部otherのインスタンスのコンテキストになっている。この場合fnameやlinenoは渡していないのでFILELINEは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"]
selfOtherOther
__FILE__"(eval)""module_eval.rb"
__LINE__414
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:厳密には、スーパークラスからサブクラスには移せる