世界線航跡蔵

Mad web programmerのYuguiが技術ネタや日々のあれこれをお送りします。

2006年11月14日

Rubyの呼び出し可能オブジェクトの比較(1)

Rubyにはコード片を表すオブジェクトが複数ある。Method, UnboundMethod, Procである。Continuationは少し違うけど、実行コンテキストを記憶しているオブジェクトという意味では近いものがあるか。『Ruby Way』にはこういういろいろがあることについて「驚くほどのことではありません」と書いてあるけれども私は驚いた。で、これらが微妙に違うのだ。困ったもんだ。いや、便利なのかもしれないが。

それで今回はこれらの概要を眺めてみたいと思う。

普通のメソッド

defでメソッドを定義するのが一番普通だやな。

class C
  def greeting(arg)
    puts "C#greeting reveived #{arg}"
  end

  def iterator
    yield 'iterator 1st'
    yield 'iterator 2nd'
    yield 'iterator 3rd'
  end 

  local = 1
  def ref_local
    puts local
  end
end

obj = C.new

# まぁ、普通に呼べる
obj.greeting 1     # => C#greeting received 1

# 引数の数はチェックしてくれる。
obj.greeting 1, 2
  # => ArgumentError: wrong number of arguments (2 for 1)

# ブロック付き呼び出し構文がRubyの特徴。ブロックかわいいよブロック
obj.iterator do |item|
  puts item
end
  # => iterator 1st
  #    iterator 2nd
  #    iterator 3rd


# defの外側のローカル変数は参照できない。JavaScriptとかの感覚からするとこれがださい。
obj.ref_local
  # => NameError: undefined local variable or method `local' for #<C:0x5e1b8>

Methodオブジェクト

Rubyのメソッドはオブジェクト化できる。特定のメソッドに結びついた呼び出し可能なメソッドを表すのがMethodオブジェクトだ。Methodオブジェクトを取得するには、既存のインスタンスのmethodメソッドを、メソッド名をシンボルか文字列で渡して呼ぶ。

greeting = obj.method(:greeting)

括弧を省略可能なメソッド呼び出し構文のせいで、JavaScriptやPythonのようにobj.greetingと書いてメソッドオブジェクトを取得するという訳にはいかない。Methodオブジェクトを表す自然な表記が存在しないあたりが、Rubyではメソッドはファーストクラスのオブジェクトでないと言われる理由の1つだ。

greetingは普通のメソッド呼び出し構文では呼べない。今、Methodオブジェクトを参照しているgreetingはローカル変数だけれども、メソッド呼び出しはメソッド用の名前空間を探索するからNoMethodErrorになる。Rubyの場合、変数とメソッドは名前空間を共有してないのだ。

greeting(1)  # => NoMethodError: undefined method `greeting' for main:Object

ちなみにgreetingとだけ書いても引数の数でエラーになったりはしない。すぐにわかるように、これはローカル変数greetingだけを含む式として評価される。

greeting     # => #<Method: C#greeting>

Methodオブジェクトを呼び出すにはcallか[]を使う。[]がオーバーライドされてるのは、通常の括弧付き呼び出しに少しでも近い表記で、ということ。

greeting.call 1 # => C#greeting received 1
greeting[1]     # => C#greeting received 1

引数の数もちゃんとチェックしてくれる。

greeting[1, 2]  # => ArgumentError: wrong number of arguments (2 for 1)

ブロックも渡せる

iterator = obj.method(:iterator)
iterator.call do |item|
  puts item
end
  # => iterator 1st
  #    iterator 2nd
  #    iterator 3rd
呼び出し構文

どうにも、普通のメソッドのようには呼べないのは悩ましい点で、まつもとさんも悩んでる様子。「callを省略する」とか。Ruby 1.9では行ったり来たりしてる。

parse.y version 1.372

parse.y version 1.372の変更。ローカル変数に括弧をつけると、暗黙にcallに変換れされるようになった。

greeting("v 1.372")    # => C#greeting received v 1.372
greeting "v 1.372"     # => C#greeting received v 1.372

括弧の省略もできる。おぉ。すばらしい。

parse.y version 1.382

parse.y version 1.382。暗黙の変換はやめ。ローカル変数のほうにも括弧をつけれたときのみ、括弧で呼び出せることになった。

(greeting)("v 1.382")   # => C#greeting received v 1.382

この構文はC言語の関数ポインタが元ネタな気がする。Ruby処理系のコードはK&Rスタイルだし。ちなみに、どちらの括弧も省略はできない。

greeting(1)   # => undefined method `greeting' for main:Object (NoMethodError)

(greeting) 1  # => parse error
やっぱりやめ

parse.y version 1.442の変更で、この呼び出し構文は無くなった。

(greeting)(1)  # => parse error, expecting `$'
___send__との比較

Methodオブジェクトの呼び出しはどうにもRubyにしてはいまいち構文が格好悪いのだけれども。でも実はあまり困らない。なぜかというとそもそもMethodオブジェクトをあまり使わないから。処理先のメソッドを切り替えるだけなら、オブジェクトに送信するメッセージを保存しておいて__send__する方が楽なのだ。

msg1 = [:greeting, 1]
msg2 = [:greeting, "Hello"]
msg3 = [:inspect]

obj.__send__(*msg1) # => C#greeting reveived 1
obj.__send__(*msg2) # => C#greeting reveived Hello
obj.__send__(*msg3) # => "#<C:0x2aa33c>"

Methodオブジェクトの利点は、それがメソッド本体のコード列への参照であるということだ。だから、Methodオブジェクトを保存しておけば、メソッドを書き換えてしまった後でも、削除した後でも元のメソッドを利用できる。

greeting = obj.method(:greeting)
class C
  def greeting(arg)
     puts "Yet another ruby hacker"
  end
end

obj.greeting("test")  # => Yet another ruby hacker
greeting.call("test") # => C#greeting received test

class C
  remove_method :greeting
end

obj.greeting("test")
    # => NoMethodError: undefined method `greeting' for #<C:0x2aa33c>
greeting.call("test")
    # => C#greeting received test

__send__の場合、オブジェクトに送りたいメッセージを保存しておいて、その都度メソッドディスパッチしているだけなので、メソッドの定義を書き換えてしまうと書き換え後の呼び出しは影響されることになる。Methodオブジェクトの場合は、書き換える前のメソッドの定義そのものをポイントしているオブジェクトなので、呼び出すときにはメソッドディスパッチは発生しない。それはObject#methodを呼んだときに発生してしまったのだ。だから、後からメソッドを書き換えても、書き換え前の定義で動作する。

UnboundMethod

Methodは、メソッド定義とレシーバーを組にしたようなオブジェクトだ。ここからレシーバーを抜いたのがUnboundMethodだ。C#のdelegateに近いのがMethodオブジェクトだとすれば、C++のメンバ関数ポインタに一番近いのがUnboundMethodだ。C++だとMethod相当のものはメンバ関数ポインタを使ってboostあたりのFunctorみたいに実装するよね。

UnboundMethodはModule#instance_methodかMethod#unbindで作成する。

u_greeting = greeting.unbind
u_iterator = C.instance_method(:iterator)

Rubyプログラムではどこにでもselfがいるのであった。一見関数に見えるものもRubyでは全部オブジェクトへのメッセージ送信だから、レシーバーのないUnboundMethodは実行できない。

UnboundMethodにはMethodと同じcallや[]が定義されているけれども、これは多分、Ruby 1.6の頃、UnboundMethodがMethodのサブクラスだったときの名残。どっちみち、callや[]を呼んでもTypeErrorが発生する。

で、仕方がないから、呼び出すときはUnboundMethod#bindでレシーバーを設定してから使う。UnboundMethod#bindはMethodオブジェクトを返すので、使い方はさっきと同じ。

iterator2 = u_iterator.bind(C.new)
iterator2.call do |item|
  puts item
end

UnboundMethodを、元のメソッドが所属していたのとは違うクラスのインスタンスにbindすることはできない。UnboundMethod経由でクラスからクラスへメソッドをコピーできたら便利なのに。実際には

u_iterator.bind(OtherClass.new)

とかやると、TypeErrorになる。まぁ、C言語で実装されているメソッドなんかは型にシビアだったりするから、変にコピーされると危ないのかもしれない。

MethodとUnboundMethodの使い分けはまあ、自明というか。Methodはselfもセットで持ち歩きたい時に使う。UnboundMethodはselfはいらないとき、むしろselfは使うときに初めて決まるような場合に使う。Methodはインスタンスが先立っていないと作れないけど、UnboundMethodはクラスオブジェクトから作れる、とか。

ちなみに、Ruby処理系内部の実装では、MethodもUnboundMethodもどちらもstruct METHODだ。UnboundMethodはselfを記録しておくポインタを使ってないだけ。

普通のProc

さてさて。次はProcか。Procオブジェクトはコード片を表すオブジェクトで、ブロックを元にして作成するのが普通だb。Proc.newまたは同義語であるKernel#lambdaで作成する。これらは、ブロック付きで呼び出すとそのブロックの部分をProcオブジェクトにまとめてくれるメソッドだ。

内部的にはProcオブジェクトとメソッド呼び出しにくっついてるブロックは少し違うもので、Rubyの古いバージョンでは大域ジャンプまわりで挙動が少し違ったりしていた。breakしたときとか、retryしたときとか。Procのドキュメントに詳しく書いてある。

ProcはMethodやUnboundMethodとは毛色が違う。元になるメソッドが存在しないとか、特定のオブジェクトに属する意味合いが薄い(同じことか)っていうのが大きいけど、それ以上に基本的にはcloureである点が違う。

def create_closure
  counter = 0
  Proc.new { p counter += 1 }
end

c = create_closure
c.call     # => 1
c.call     # => 2
c.call     # => 3
....

create_closureメソッドを抜けた時点で既にローカル変数counterの属するスコープは存在しない。けれども、Procオブジェクトは内部に関連するローカル変数を記憶していて、こうやってProcオブジェクトの中からは、そのオブジェクトが消滅するまでローカル変数にアクセスできる。

Procオブジェクトが内部に持ってるのは、そのProcが作られたときのローカル変数環境であって、次回以降のcraete_closure呼び出しで新規に作られる環境とは関係ない。

c1 = create_closure
c1.call     # => 1
c1.call     # => 2
c1.call     # => 3

c2 = create_closre
c2.call     # => 1
c2.call     # => 2
c1.call     # => 4
c1.call     # => 5
c1.call     # => 6
c1.call     # => 7
c2.call     # => 3
c1.call     # => 8
c2.call     # => 4

c1が参照しているcounterとc2が参照しているcounterは別物だ。まぁ、インスタンス変数みたいなもんだ。

で、何がうれしいのかというと、ベタにCurry化とか? そういえば、Procオブジェクトそのものを操作するプログラムってあまり書かないね。

こういうのはある。2つのオブジェクトに変数を共有させたいけど、グローバル変数やクラス変数にはしたくないとき。

def create_twin
  shared = 0
  return [
    Proc.new { p shared += 1 },
    Proc.new { p shared += 1 }
  ]
end

dee, dum = create_twin
hikaru, kaoru = create_twin

dee.call    # => 1
dum.call    # => 2; deeとdumはsharedを共有

hikaru.call # => 1
kaoru.call # => 2; こっちはまた別のものを共有

まぁ、RubyよりはPerlでよく見る手法だけれど。あと、JavaScriptでプライベートなメソッドを定義する1つの方法ははこの手法の応用だよね。AjaxまわりをやるとClosureのありがたみは自然に体験できるはず。

(2006-11-17: ma2さんの指摘によりプログラム例のバグ修正)

ProcもMethodと同じく引数を取れる。実引数は、ブロック引数に渡るので次のように書く

sum = 0
acc = Proc.new{|num| p sum += num}

acc.call(3) # => 3
acc.call(2) # => 5
ブロック構文

で、ProcはProc.newによって生成するよりは、ブロック付きメソッドの実装に使う場合が多い気がする。

def iterator(a, b, &block)
  ...
end

とか書いておいてこのメソッドをブロック付きで呼ぶと、仮引数blockに渡したブロックをProc化したものが入る。例えば、これは特定のオブジェクトの特異クラスのコンテキストでブロックを評価するメソッド。

def singleton_class_eval(&block)
  (class << self; self end).class_eval(&block)
end

ここでは受け取ったブロックをそのままclass_evalに渡してしまってるから、あまりオブジェクト化してるありがたみはないかもしれない。でも一度オブジェクトとして受け取ってしまえばいくらでも加工できるわけだから、夢がひろがる。

DSL

これがRailsのDSLになると、受け取ったProcオブジェクトをインスタンス変数に保存しておいて必要なときに起動するようなのを多用してる。RubyKaigi2006でしゃべらせてもらったときに話したけれど、Railsの宣言的な表記が可能なのは、宣言文を実行するときとブロック部分を実行するときが別々だから。例えば、validationに付ける:ifオブション。

class UserRegistration < ActiveRecord::Base
  validates_presence_of :phone, :if => Proc.new{|reg| reg.stage >= 1}
  ....
end

例えば、ユーザー登録が複数の画面に分かれてて、最初の画面ではvalidates_presence_of :phoneを有効にしたくないとしたら、画面の番号をstage属性に入れておくとして、こんな風に書ける。ActiveRecordは引数として受け取ったProcオブジェクトを内部に保存しておいて、UserRegistrationのvalidationが走るたびにUserRegistrationのインスタンスを引数としてProcオブジェクトを実行する。Proc.newを実行するのはクラスの定義時だけど、Proc.newに渡したブロックの中身はもっと後で実行される。

呼び出し表記

Procオブジェクトの内部のコードを呼び出すには、MethodやUnboundMethodと同様の[]とcallがある。そして、その他にProc#yieldがある。

[]とcallはMethodやUnboundMethodの場合と同じく引数の数をチェックしてくれる。けれども、yieldは引数の数をチェックしない。これはブロック付きメソッドの中でyield文を実行する場合と同じだ。例えば、

100.times do
  print "Hello"
end

Kernel#timesはブロックにカウンタの値を渡すけれども、こんな風にカウンタの値は使わない場合も多い。多分それで、ブロックをyieldするときには引数の数のチェックが入らないのだと思う。毎度毎度必ず 100.times do |i| と書かねばならないのは嫌だ。そして、Proc#yieldのほうはそれにあわせた仕様になっている。

Procの使いどころ

先にも挙げたけれどもProcを使う一番ありがちなケースはブロックを受け付けるメソッドを実装するとき。

それから、上のcreate_twinの例のように、closureとしてのProcは安心をもたらす。何故かというと、Procが参照している外側のローカル変数は、Procを生成したコンテキストを抜けてしまったら最後、Proc以外からは参照できないから。書き換わる心配もない。create_twinを抜けてしまったら、その後は生成されたProcのペア以外からはcounterを参照することはできない。厳密には抜け道はあるけれど、その場合はそうと分かるコードになるから大丈夫。

これをインスタンス変数で代用しようとすると困ったことになる。インスタンス変数はそのインスタンス内では自由にアクセスできてしまうから。特にサブクラスとのインスタンス変数名の衝突は困った問題で、Ruby 2.0では改善される可能性もあるけれど現状はどうしようもない。

誰がどんな拡張をするのか分からないライブラリを作る場合、インスタンス変数よりは敢えてclosureの外部スコープ参照を使う場合がある。

Continuation

Continuationの概念は分かってしまえば非常に自然なのだけど、説明するのはとても難しい。多分、Continuationを初めて使い覚えるにはRubyよりSchemeのほうがいい。私も『プログラミング言語SCHEME』を読むまでRubyのContinuationを使えなかったし。

Rubyの特徴の一つは標準でContinuationをサポートしていることだとは思う。Parrotも確かRuby移植を見据えてContinuationを実装したとか言ってたし。でも、実は有効に活用されている事例はそんなに多くない。まつもとさんも「実装できたから作ってしまった」といってるぐらいで積極的にサポートされてる感じではない。ささださんがYARVへのContinuation導入にあまり乗り気でないのでRuby 2.0で消える可能性もないではない。そうして欲しくないので私はささださんの顔見るとContinuationプリーズと言ってるけれども。

Continuationは、「ここから先、プログラム終了までの処理」を表すオブジェクトである。作成にはKernel#callccを使う。

cont = nil
callcc {|c| cont = c } 
puts :ok
exit

例えば、こんなコードを考えてみる。callccは

puts :ok
exit

という、「ここから先の処理」を表すContinuationオブジェクトをブロック引数としてブロックを評価する。ここでは、ブロック内でオブジェクトを保存している。これで何が嬉しいかというと、保存しておいたContinuationオブジェクトをcallすれば、いつでもcallccの次から処理を再開できるのである。しかも、ローカル変数のコンテキストはそのまま保存されてる。

上では「ここから先の処理」がややこしくならないように恣意的にexitを置いてプログラムを終了させている。けれども、そう簡単でない場合の方が多い。そのときにこそContinuationが本領を発揮するのだ。

例えば、こんなのはどうだろう。RubyKaigiの時のプレゼンに書いたコードのバグフィクス版だけど。

@cont = []
ActiveRecord::Base.transaction do
  catch :save_tx do
    collection.each do |item|
      ....
      callcc{|c| @cont.push c; throw :save_tx} if something?
      ....
    end
  end
end
unless @cont.empty?
  ActiveRecord::Base.transaction do
    @cont.pop.call
  end
end

ActiveRecord::Base.transactionは、データベーストランザクションを開始してブロックを評価し、ブロックが正常終了するとトランザクションをコミット、例外発生時にはロールバックするメソッドだ。でも、何かの条件が成立したときには早めにコミットしてしまって、更にそこから引き続き処理したい場合がないだろうか。上は「一回コミットしてしまう」機能を付け加えたものだ。

  • 2行目、まず、普通にActiveRecord::Base.transactionに入る。
  • 3行目、catchを使ってネストからの脱出を準備する。これはJavaやなんかのラベル付きbreakと同じで、ネストしたブロックや制御構造から一気に脱出するものだ。次と同じだと考えて良い。
save_tx:
  for (Object item : collection) {
     ....
  }
  • 4行目から。さて、では、あるcollectionの各要素について処理をして、データベース操作をする。
  • これを各要素について繰り返す
  • 6行目。そして、ある条件が成立したとき、callccを実行する。
  • callccは、その次の行からの、もしcallccが書かれていなければ行われていたであろう処理をContinuationオブジェクトに変換し、ブロックを評価する
    • ブロックはそれを@contの中に保存し、Kernel#throwで先ほどのKernel#catchで作った:save_txラベルに向けて脱出する
  • 9行目。すると、catchのブロックを抜けるとその後には何もないから、そのままActiveRecord::Base.transactionのブロックも抜けてしまう。正常に抜けたことになるので、ActiveRecord::Base.transactionはトランザクションをコミットする。
  • 11行目。さて、@cont.empty?であるとは、callccではなく、普通にcollectionのeachが終わって抜けてきたことを意味する。さきほどcallccの中で@contにcontinuationを代入したから、今はempty?でない。
    • 12行目。すると、ActiveRecord::Base.transactionを利用して次のトランザクションを開始する。
    • 13行目。@contの要素を取り出して、それをcallする。
      • popされたcontinuationは「callccの次からの処理」を覚えているから、必要ならローカル変数を復元して、callccの次の処理(7行目)へ制御を移す
      • そして、気がつくとまたcollection.eachの中にいるので繰り返しを始める(8行目)
      • また、something?が満たされれば同じことを繰り返す(6行目)
      • いつかはcollection.eachのほうが終了する(8行目)
      • 10行目。そのときは、やはりActiveRecord::Base.transactionはブロックが正常終了したと言うことでコミットする。
      • 11行目。今度は@contは空なので処理を終わる

ややこしいだろうか。ややこしいと思う。こんなの分かってるよ、という人以外は練習を兼ねて、まつもとさんが書いたCGIプログラムをそのままFastCGI化するライブラリを読むと良いと思う。

呼び出しかた

Continuationオブジェクトはやはり[]とcallによって呼び出せる。呼び出しが可能で、コンテキストを記憶しているっていう意味でContinuationはやはりMethod/Procの仲間ではあるけれども、中身はかなり違うし、それにContinuation#callは原則的に制御を返さない。

ちなみに、callや[]に渡した引数はArrayに束ねた形で、戻ってきたときのcallccの評価値になる。これを使って最初にcallccを実行したときとContinuation#callで戻ってきたときを振り分けることもできるだろうし、戻ってきた環境に情報を渡すこともできる。Cを知ってる人はsetjmpの戻り値だと思えば良いと思う。

Continuationの使いどころ

Continuationは慣れるととても自然な概念だ。本当に、「ここから先の残り」「続き」だから。続きを保存しておいて制御を一時的に余所へやりたいときに便利である。Threadでもできるんだけれども、ThreadよりもContinuationの方が自然だし。というか、ThreadはContinuationで実装できるものだ。プリエンプティブなThreadを実装するのは割と面倒だけど。

ContinuationとMethod/UnboundMethod/Procの使い方を迷うことはないと思う。たぶん。やっぱりContinuationは変わり者なんだな。

予告

とまぁ、以上は私が書きたかったことではなくて、前置きだったんだな。前置きがふくらんでしまったのでそのまま公開しますけれども。次回はこれらの呼び出し可能オブジェクトを弄って遊んでみる。

トラックバック

http://yugui.jp/articles/541/ping

現在のところトラックバックはありません

コメント

ma2 (2006年11月16日 13時25分33秒)
<p>自分の無知を晒すようでアレなんですが Proc.new { p tweedle += 1 } は Proc.new { p shared += 1 } ではないんですか?</p>
Yugui (2006年11月17日 19時11分51秒)
<p>&gt; ma2さん<br />そうですね。コピペ後に修正したときの修正忘れです。ありがとうございました。</p>
no-name (2007年03月21日 05時49分03秒)
<p>&gt; acc = Proc.new{|num| p sum += sum}<br />これは<br />acc = Proc.new{|num| p sum += num}<br />ではないでしょうか?<br />(最後のsum が numになる)</p>
Yugui (2007年03月21日 13時16分25秒)
<p>&gt; no-nameさん</p> <p>あうー。まだミスがありましたか。ありがとうございます。</p>
blog comments powered by Disqus

ご案内

前の記事
次の記事

タグ一覧

過去ログ

  1. 2015年07月
  2. 2015年06月
  3. 2015年05月
  4. 2015年04月
  5. 過去ログ一覧

フィード

フィードとは

その他

Powered by "rhianolethe" the blog system