メイン

4章 アーカイブ

2007年09月25日

4章.参照渡しと値渡し (その1)

今日から4章(p.55~)に入ります。「参照渡しと値渡し(pass by reference, pass by value)」とは、今までにやった内容をさらに深く踏み込むようなタイトルがついている章ですね。この章で主に説明されているのはオブジェクトの渡し方についてです。すごくワクワクします。うん。それでははじまりはじまり。


Rubyの場合

  1. 参照渡し
  2. Rubyの場合は、基本は参照渡しです。本に記載されているスクリプトを抜粋します。

    % irb --prompt simple
    >> def foo(str); str.chop!; end
    >> my_str = "Hello, World."
    >> foo(my_str)
    >> my_str
    => "Hello, World"
    >> foo(my_str)
    >> my_str
    => "Hello, Worl"
    

    foo(my_str)とすると、my_strの内容が複製されて渡されるわけじゃなく、my_strの参照が渡されます。chop!ってのはStringクラスのメソッドで、文字列の最後の1文字を取り除きます。chop!は、渡されたオブジェクトの内容を書き換えますので、foo(my_str)でmy_strの方も変わっちゃうということです。(!がメソッド名の末尾についているやつは、大抵自分自身の内容を直接変えるやつのようです。)

    ちなみに、代入も参照の代入なので

    my_str = "Hello, World."
    my_str2 = my_str
    my_str2.chop!
    

    とすると、my_strも変わりますね。

  3. 値渡しその1:浅いコピー(shallow copy)
  4. 値渡しをしたいときは、dupで複製を作って渡すようです。以下も本からの抜粋です。

    >> my_str = "Hello, World."
    >> foo(my_str.dup)
    => "Hello, World"
    >> my_str
    => "Hello, World."
    >> foo(my_str.dup)
    => "Hello, World"
    >> my_str
    => "Hello, World."
    

    はい、出来ました。なお、dupはオブジェクトを複製するメソッドですが、浅くコピる(インスタンス変数などまでは複製しない)ようです。2章で本に登場したReminderクラスを使って試してみます。Reminderクラスとは、本のp.16に載っているreminder0.rbのことです。今回はRubyの実験なのでdRubyは使いませんよ。

    irb(main):002:0> r = Reminder.new
    => #<Reminder:0x2927ce4 @serial=0, @item={}>
    irb(main):003:0> r.add('hogehoge')
    => 1
    irb(main):004:0> r.to_a
    => 1, "hogehoge"
    irb(main):005:0> r2 = r.dup
    => #<Reminder:0x29a1abc @serial=1, @item={1=>"hogehoge"}>
    irb(main):006:0> r.__id__
    => 21577330
    irb(main):007:0> r2.__id__
    => 21826910
    irb(main):008:0> r[1].__id__
    => 21564760
    irb(main):009:0> r2[1].__id__
    => 21564760
    irb(main):010:0> r2.delete 1
    => "hogehoge"
    irb(main):011:0> r.to_a
    => []
    irb(main):012:0> r2.to_a
    => []

    r(Reminderのインスタンス)と、rをdupして作成したr2はそれぞれ別のIDになっていますが、インスタンス変数である@itemの各要素までは複製されていないため同じIDを返します。なので、r2で要素を消すとrにも影響を与えます。

  5. 値渡しその2:深いコピー(deep copy)
  6. Rubyで深いコピーを実現したいときには、Marshalを使うようです。Marshalはオブジェクトを文字列に書き出したり、それらからオブジェクトに復元したりするもの(モジュール)です(http://www.ruby-lang.org/ja/man/?cmd=view;name=Marshal

    先ほどirbで書いたスクリプトでdupしている部分をMarshalに変えてみます。

    irb(main):003:0> r = Reminder.new
    => #<Reminder:0x2921498 @serial=0, @item={}>
    irb(main):004:0> r.add('hogehoge')
    => 1
    irb(main):005:0> r.to_a
    => 1, "hogehoge"
    irb(main):006:0> r2 = Marshal.load(Marshal.dump(r))
    => #<Reminder:0x2999e0c @serial=1, @item={1=>"hogehoge"}>
    irb(main):007:0> r.__id__
    => 21563980
    irb(main):008:0> r2.__id__
    => 21810950
    irb(main):009:0> r[1].__id__
    => 21832900
    irb(main):010:0> r2[1].__id__
    => 21810890
    irb(main):011:0> r2.delete 1
    => "hogehoge"
    irb(main):012:0> r.to_a
    => 1, "hogehoge"
    irb(main):013:0> r2.to_a
    => []

    変えた部分は赤字の部分のみです。Marshalを使うとごっそり複製されるので、r2で要素を消してもrに影響がありません。

dRubyの場合

ではdRubyの場合はどうなのかと言うと

dRubyではRubyのクラスライブラリMarshalを用いて他のプロセスにオブジェクトを渡します。

4.1.2 dRubyでは?

dRubyの場合は、先ほど試してみたMarshalの仕組みを使って、オブジェクトを文字列化して転送しているようです。なので、値渡しということですね。今日はここまで、続きはまた次回!

2007年10月03日

4章.参照渡しと値渡し (その2)

はい、では4章の続きです。今度はdRubyでのオブジェクトの渡し方です。基本は本の通りに進めますが、「2章.Hello, dRuby.」で使ったReminderクラス(p.16のreminder0.rb)を使って試そうと思います。多分このクラスが一番馴染み深いと思うからね。じゃあはじめます。

dRubyの場合

dRubyではRubyのクラスライブラリMarshalを用いて他のプロセスにオブジェクトを渡します。 Marshalは任意のオブジェクトをバイト列に直列化するdumpメソッドと、直列化されたバイト列からオブジェクトを再現するloadメソッドからなります。 MarshalはRubyでdeep copy(深いコピー)を行うのにも利用できます。

4.1.2 dRubyでは?

とあるので、前回の最後に書いたとおり、dRubyの場合は値渡しということになりそうです。本ではここでMarshalの実験をしています。が、このサイトでは、まずReminderを使った別の実験をしてみます。というわけで、何回か前のエントリでやったように電文を見ます。今回はデータ部の中身までじろじろ見ていきます。TCPパケットは能弁に語る、気がする。

サーバー側は、2章で登場したサーバーをそのまま使います。クライアント側は

require 'drb/drb'
r = DRbObject.new_with_uri('druby://hoge')
r.add('foo')
str = r[1]

とやり、パケットをダンプしました。
まずは

r.add('foo')
を見てみます。この送信電文は以下の通りです。色が変わっている部分がTCPのデータ部です。

うーん、さっぱりわかんね。fooとかaddとか断片的にわかりますが、転送されるデータの内容(データ部の内訳)は謎です。仕方ないので送信部分のソースも見ていくことにします。

まず、DRbMessageの送信部分です。

def send_request(stream, ref, msg_id, arg, b) # :nodoc:
 ary = []
 ary.push(dump(ref.__drbref))
 ary.push(dump(msg_id.id2name))
 ary.push(dump(arg.length))
 arg.each do |e|
  ary.push(dump(e))
 end
 ary.push(dump(b))
 stream.write(ary.join(''))
・・・

です。最後につけられるbってのがわからなかったので調べました。これはmethod_missingの引数の一部みたいですが、どうやらブロック(do … end)が渡されたときにはこっちに入るようです。ブロックを渡すようなことを当面はしそうに無いので、今回はあまり深く考えずに置いておきます。
さらに、このメソッドの中で各所で呼ばれているdumpメソッドの内容も見てみます。

def dump(obj, error=false) # :nodoc:
 obj = make_proxy(obj, error) if obj.kind_of? DRbUndumped
 begin
  str = Marshal::dump(obj)
 rescue
  str = Marshal::dump(make_proxy(obj, error))
 end
 [str.size].pack('N') + str
end

ここで一番重要な処理は

[str.size].pack('N') + str

ではないかと思われます。strとは、オブジェクトを文字列に符号化したもの(マーシャルデータ)です。ここではそのstrの前に、strのサイズ(バイト数)を固定長のバイナリデータとしてくっつけています。リファレンスを見てみると、pack('N')とは、big endian unsigned 32bitの形式で、バイナリにパックするメソッドです。で、こいつはプラットフォームに非依存なので、エンディアンの変換は気にしなくてもいいということですね。マーシャルデータの前にサイズをつけているのは、どこまでを読んで復元してよいのかわからないからです。可変長のデータを送受信するときの常套手段かな(それか最後に、“ここまで”という記号をつけて送るかどちらか。)

これでデータ部をだいたい理解できるようになったはず。ではデータ部の先頭から順番に見ていきます。ここはオプション引数が入っているはずの部分です。

00 00 00 03 04 08 30

先頭から4バイトが、データ長をpack('N')したものです。03なので、ここから3バイトがオブジェクトの内容だと言う事だと思います。内容は04 08 30ですが、これはnilってことみたいです。実際に以下のようなコードを実行してみると同じ結果になります。

obj = nil
Marshal::dump(obj).each_byte do |b|
  puts sprintf("%x",b)
end

今回はオプション引数が無いのでnilのようです。もし「2章.Hello, dRuby (その5)」で試したようにオプション引数を使っていればその文字列が渡されてくるのだと思います。では次。

00 00 00 07 04 08 22 08 61 64 64

先頭4バイトは7なので、それに続く7バイトを見ます。04 08 22 08 61 64 64は'add'をマーシャルデータ化したものです。最後の61 64 64は、ASCIIで'add'ですね。04 08 22 08 はよくわかんないけど、文字列だよ、みたいなことを教えてる情報なんでしょうきっと。

続きも見ていきます。

00 00 00 04 04 08 69 06

これは、数値の1のマーシャルデータです。引数の個数を示します。

00 00 00 07 04 08 22 08 66 6f 6f

次に、上記の個数分だけ引数の内容が続きます。今回は、'foo'という文字列のオブジェクトのマーシャルデータです。(66 6f 6fが'foo'です。)複製されたオブジェクトが渡されています。なので確かに値渡しですね。

 ということで、以下がdRubyの(通常の)TCP送信時の電文形式のようです。各々先頭に、サイズを示す4バイトの固定長バイナリが付加されます。

1. オプション引数
2. メソッド(メッセージ)名
3. 引数の数
4. 引数の内容(3の数だけ存在)
5. ブロックの情報

以上です。次回は、サーバーからの応答(メソッドの戻り値)を見ていきたいと思います。

2007年10月04日

4章.参照渡しと値渡し (その3)

前回のエントリでは、メッセージ呼び出し時の引数について値渡しの確認をしました。今回は応答を見ます。メソッドの戻り値というやつです。前回と同様に2章で利用したリマインダーのdRubyサーバーを立ち上げておき、以下のスクリプトを実行してその中身を見ます。

require 'drb/drb'
r = DRbObject.new_with_uri('druby://hoge')
r.add('foo')
str = r[1]

このスクリプトのうち、前回は

r.add('foo')

を見ました。今回はその次のステップである

str = r[1]

を見ていくことにします。まず送信部分のTCPデータ部です。前回と同じように、色が変わっている部分がデータ部です。

見てわかるとおり、r[1]もメソッドです。([]というメソッドに、引数:1が渡される)ので、前回のadd('foo')の時と殆ど変わりはありません。解説は省略します。
次はサーバーから返されるデータ(クライアントが受信するデータ)のデータ部の内容を見ていきます。これも色が変わっている部分がデータ部です。

データ部は18バイトしかありません。短いですね。先頭から見ていきます。

00 00 00 03 04 08 54

これはboolのtrueを表します。リモートのメッセージパッシングが成功したらtrue。失敗したらfalseが入ってくるようです。

で、これに続くのが、

00 00 00 07 04 08 22 08 66 6f 6f

これは、'foo'という文字列のオブジェクトのマーシャルデータです。(66 6f 6fが'foo'です。)複製されたオブジェクトが渡されています。

なお、DRbMessageの以下のメソッドが、該当の処理を行なっている部分だと思います。

def send_reply(stream, succ, result) # :nodoc:
 stream.write(dump(succ) + dump(result, !succ))
rescue
 raise(DRbConnError, $!.message, $!.backtrace)
end

このことからdRubyのメソッドの戻り値の電文形式は以下の通りとわかりました。各々先頭に4バイトのサイズを示す固定長バイナリデータが付加される点は、送信時と同様です。

1. 成否のbool値
2. メソッドの戻り値のオブジェクト(複製)

この結果から、通常のdRubyのメッセージは、送受信共に確かに値渡しだということがわかりました。今回はここまでにします。次回は参照渡しをしてみます。

2007年10月11日

4章.参照渡しと値渡し (その4)

4章.参照渡しと値渡し (その4)

今回は参照渡しをしてみます。ちょっと本に戻ります。p.57の「4.1.2 dRubyでは?」からです。が、Marshalのあたりは、同じようなことを既に確認したような気がしますのでここでは省略します。本に沿って試してみて下さいませ。ここではその続き「p.58 参照の値渡し」から読みます。

Marshal.loadによって直列化したオブジェクトをMarshal.dumpで復元するということは、つまりオブジェクトの複製を作ることにほかなりません。ではdRubyはいつも値渡しなのでしょうか?
実は当たっているとも言えるし違うとも言えます。 dRubyでの参照渡しは、元のオブジェクトの代わりに参照情報を含むオブジェクトを直列化することで実現されます。つまり、参照渡しは参照オブジェクトの値渡しなのです。逆に、もう一方の値渡しは単純にオブジェクトを直列化するだけです。

4.1.2 dRubyでは?

ふむふむ。

Hello, World.の実験で使ったDRbObjectを覚えていますか? DRbObjectは元のオブジェクトを参照する情報をもったオブジェクトです。 DRbObjectには目的にごとに二つの生成方法が用意されています。一つはDRbObject.new_with_uriを使ってURIからリモートオブジェクトへの参照を作ることです。もう一つはDRbObject.new(obj)のようにオブジェクトを与え自プロセスのオブジェクトへの参照を作ることです。

4.1.2 dRubyでは?

ふむふむふむ。

DRbObject.new(obj)によって、任意のオブジェクトへの参照オブジェクトを作ります。参照を作るということは、他のプロセスがこの参照をつかってメソッド呼び出しする可能性があるということです。ですから、オブジェクトへの参照を作る前に、DRb.start_serviceによってdRubyのサーバを自プロセスで起動しておかなくてはなりません。

4.1.2 dRubyでは?

ふむふむふむふむふむ!!(こればっか)

元の文章でも充分わかりやすいですが、整理します。ここ、すごい重要な気がします。


  1. dRubyは、通常は値渡しだが、参照渡しにすることも出来る

  2. dRubyでの参照渡しは、厳密に言うと参照オブジェクトの値渡しだ

  3. 参照オブジェクトとは、すなわちDRbObjectのこと

  4. DRbObjectにはふたつの使い道がある

  5. 4-1. DRbObject.new_with_uriを使ってURIからリモートオブジェクトへの参照を作る
    4-2. DRbObject.new(obj)のようにオブジェクトを与え自プロセスのオブジェクトへの参照を作る

  6. 今までは上 (4-1) の使い道でしか使ってなかった。

  7. DRbObject.new(obj)で参照を作るためには、クライアントであってもDRb.start_serviceしないと駄目

  8. なぜなら、そのオブジェクトを他からリモート呼び出しすることがあるから(オブジェクトの実体はクライアントにあるから)


すげ!ちょっと感動します。

確かめてみます。p.60に掲載されているように、ハッシュを公開するサーバーを用意します。

% irb --prompt simple -r drb/drb
>> front = {}
>> DRb.start_service(nil, front)
=> #
>> DRb.uri
=> "druby://yourhost:1426"

はい立ち上げてみました。

前回の値渡しの復習をかねて、文字列をハッシュ(フロントオブジェクト)に登録してみます。

there = DRbObject.new_with_uri('druby://yourhost:1426')
str = "Hello, World."
there[1] = str

これは勿論うまくいく。では次に参照を渡してみます。参照はDRbObjectのことです。

there = DRbObject.new_with_uri('druby://yourhost:1426')
str = "Hello, World."
there[1] = str
there[2] = DRbObject.new(str)

しかしこれではうまくいきません。もしこのまま実行すると

in `current_server': DRb::DRbServerNotFound (DRb::DRbServerNotFound)

というエラーが出てしまいます。
なぜなら、DRb.start_serviceをしていないからです。クライアントからオブジェクトの参照を放り込んでいるということは実体がクライアントにあるということです。なのでメッセージに応答するのはクライアントです。クライアントが他局からのメッセージ呼び出しを受け付けることができないと駄目なので、dRubyのサービスが必要です。これがDRb.start_serviceをしなければならない理由です。

DRb.start_service
there = DRbObject.new_with_uri('druby://yourhost:1426')
str = "Hello, World."
there[1] = str
there[2] = DRbObject.new(str)

これでうまくいきます。
今日はここまで。次回は今回やった参照渡しの部分をもうちょっと詳しく見るつもりです。

2007年10月24日

4章.参照渡しと値渡し (その5)

dRubyでの参照渡しの続きです。dRubyでの参照渡しは、厳密に言うと参照オブジェクトの値渡しであることは前回学びました。参照オブジェクトとはDRbObjectのことなので、まずDRbObjectの内容を見ていくようにしましょう。
DRbObjectには二つのインスタンス変数があります。


@ref

オブジェクトの実体のID(hoge.__id__したものです。)

@url

dRubyサーバーを特定するURI


DRbObjectをnewするときに何かオブジェクトを渡すと、それが@refに入ります。例えばirbでこんなことをやってみます。

[サーバー側]

irb(main):001:0> require 'drb/drb'
=> true
irb(main):002:0> uri = 'druby://サーバーのホスト名:9898'
=> "druby://サーバーのホスト名:9898"
irb(main):003:0> h = {}
=> {}
irb(main):004:0> DRb.start_service(uri, h)
=> #<DRb::DRbServer:0x295ecd0 ・・・
irb(main):005:0> str = 'foo'
=> "foo"
irb(main):006:0> str.__id__
=> 21680810
irb(main):007:0> ref = DRbObject.new(str)
=> #<DRb::DRbObject:0x29530b0 @ref=21680810, @uri="druby://サーバーのホスト名:9898">
irb(main):008:0> h[1] = ref
=> #<DRb::DRbObject:0x29530b0 @ref=21680810, @uri="druby://サーバーのホスト名:9898">

 最後から2つ目の行(:007)を見ます。ここで作成したDRbObjectのインスタンスの内容を確認します。@refは、引数に渡されたstrというオブジェクトのIDが設定されています。@uriは、start_service時に指定したuriが入っています。DRbObjectのインスタンスを作ると、dRubyサービスのURIとオブジェクトが自動的に紐付きます。

 なお、前回のエントリで確認したように、DRbObjectのインスタンスを作る前にstart_serviceしてないとdRubyサービスが無いのでエラーになります。ただ、すっかりおなじみのnew_with_uriだけは特殊で、指定したURIのフロントオブジェクトに対する参照をもらうだけなのでstart_serviceしてなくても大丈夫です。irbをもうひとつ立ち上げて、クライアント側から(new_with_uriを使って)サーバーのフロントオブジェクトを取得するスクリプトを見ます。このエントリではこの2つのirbをそのまま使い続け、どんどん追記していきます。どちらのことか判断しやすいように、各々[サーバー側]と[クライアント側]という風に明記することにします。

[クライアント側]

irb(main):001:0> require 'drb/drb'
=> true
irb(main):002:0> there = DRbObject.new_with_uri('druby://サーバーのホスト名:9898')
=> #<DRb::DRbObject:0x29a5f68 @ref=nil, @uri="druby://サーバーのホスト名:9898">

この場合、@refはnilです。本にも

@refがnilの場合は特別にURIに関連づけられたフロントオブジェクトを参照します。

4.1.2 dRubyでは?

と記載がありますね。ちなみに前にも見たかもしれないですが、DRbObjectのnew_with_uri(uri)は、DRbObject.new(nil, uri)と同じです。

# Create a new DRbObject from a URI alone.
def self.new_with_uri(uri)
 self.new(nil, uri)
end

では、クライアントのirbから操作を続けてみます。

[クライアント側]

irb(main):003:0> s = there[1]
=> #<DRb::DRbObject:0x29a59a0 @ref=21680810, @uri="druby://サーバーのホスト名:9898">

これは、先ほどサーバーで作成したDRbObjectです。@refは21680810ですね。サーバー側のオブジェクトIDが設定されています。参照オブジェクトに対する操作をクライアント側で引き続き行ってみます。

[クライアント側]

irb(main):004:0> s.upcase!

これをしてから、サーバー側のirbを操作して中身を確認してみましょう。

[サーバー側]

irb(main):009:0> str
=> "FOO"
irb(main):010:0> ref.to_s
=> "FOO"
irb(main):011:0> h[1].to_s
=> "FOO"

upcase!は、文字をすべて大文字に書き換えるメソッドです。はい、ちゃんとサーバー側のオブジェクトの実体(IDが21680810のオブジェクト)に変更が加えられていました。めでたし。

ところで、ここでちょっと疑問です。どうやらオブジェクトのIDを知っていることで、そのIDに紐付いたオブジェクトの実体を触れるようですが、いったいどのような仕組みになっているのでしょうか。仕組みを知るためにDRbServerのソースを見てみます。

# Convert a dRuby reference to the local object it refers to.
def to_obj(ref)
 return front if ref.nil?
 return front[ref.to_s] if DRbURIOption === ref
 @idconv.to_obj(ref)
end

ここで、今回関係ある部分は「@idconv.to_obj(ref)」です。@idconvは、デフォルトではDRbIdConvのインスタンスが設定されているようなので、このクラスを見ることにします。

class DRbIdConv

# Convert an object reference id to an object.
#
# This implementation looks up the reference id in the local object
# space and returns the object it refers to.
def to_obj(ref)
 ObjectSpace._id2ref(ref)
end
・・・

ここではObjectSpaceというものを使って変換しているようです。Rubyのリファレンスを見てみます。

ObjectSpace
全てのオブジェクトを操作するためのモジュール。

モジュール関数:
ObjectSpace._id2ref(id)
オブジェクト ID(Object#__id__)からオブジェクトを得ます。対応するオブジェクトが存在しなければ例外 RangeError が発生します。

Rubyリファレンスマニュアル - http://www.ruby-lang.org/ja/man/index.cgi?cmd=view;name=ObjectSpace

ObjectSpaceモジュールとは、どうやらRubyスクリプト内で実体化されたすべてのインスタンスの集合に対して色々できるやつのようです。なるほど、こういうやつが居るんですね。わかったので先へ進みます。

では、次はクライアントのirbから参照を渡します。ちゃんとstart_serviceしておきます。引数を指定しない場合、空いている適当なポート番号が割り当てられます。ここでは30273が割り当てられました。

[クライアント側]

irb(main):005:0> DRb.start_service
=> #<DRb::DRbServer:0x296448c ・・・・
irb(main):006:0> str = 'bar'
=> "bar"
irb(main):007:0> there[2] = DRbObject.new(str)
=> #<DRb::DRbObject:0x2937ce8 @ref=21696060, @uri="druby://クライアントのホスト名:30273">


そしてサーバー側で内容を書き換えます。

[サーバー側]

irb(main):012:0> h[2]
=> #<DRb::DRbObject:0x297898c @ref=21696060, @uri="druby://クライアントのホスト名:30273">
irb(main):013:0> h[2].upcase!
=> "BAR"

クライアント側の実体が書き換えられていることを確認します。

[クライアント側]

irb(main):008:0> str

=> "BAR"


はい、ちゃんと書き換えられていますね。最後に、ここでクライアントのirbを終わらせてから、クライアントが実体を持っている参照オブジェクトに対してサーバーから操作してみましょう。エラーになるはずです。

[サーバー側]

irb(main):014:0> h[2].to_s
DRb::DRbConnError: druby://クライアントのホスト名:30273 - #<Errno::EBADF: Bad file descriptor - connect(2)>

 DRbObjectについての理解がだいぶ深まったような気がします。なお、このエントリでは、本の中で紹介しているものとほぼ一緒の内容を試していますが、細かな手順が結構違っています。なのでこのエントリを読まれた方も是非一度、本の「4.1.2 dRubyでは?」に載っている手順に沿っておさらいしてみて下さい。次回はその続き、「4.2 自動的な参照渡し」に入るつもりです。

2007年10月29日

4章.参照渡しと値渡し (その6)

はい、今回は「4.2 自動的な参照渡し」から読み続けます。

4.2 自動的な参照渡し

参照渡しにするには、いつも明示的にDRbObject.new()しなくてはならないのでしょうか?なんだか面倒ですね。 dRubyでは明示的に参照渡しをしなくても、ライブラリが自動的に判断して参照渡しと値渡しを選択する仕組みが用意されています。

dRubyでは次の規則で参照渡しと値渡しを選択します。

  • Marshal.dumpできるものは値渡し
  • Marshal.dumpできないものは参照渡し

4.2 自動的な参照渡し

ほうほう、今までは、参照渡しにするために明示的にDRbObject.newをしていました。ですが、Marshal.dumpできないものは参照渡しとなるわけですね、なるほど。本の「4.2.1 can't dump」あたりを読みながら実験してみます。本ではMarshal.dumpできない$stdoutを使って試していますね、では、本の通りにやってみます・・・はい、できました。(はやっ)

4.2.1 can't dump

front[:stdout]はIOではなく、DRbObjectとなりました。 DRbObjectの@uriはターミナル2のDRb.uriです。 front[:stdout]のDRbObjectはターミナル2のオブジェクトを参照するオブジェクトであることがわかります。

何がおきたのでしょうか?ターミナル2から$stdoutをターミナル1に渡す時、 dRubyのライブラリはMarshal.dumpが失敗したことを捉えて値渡しの代わりに参照渡しを行なったのです。

4.2.1 can't dump

にゃるほど、多分これに該当する箇所としてはDRbMessageの

private
def make_proxy(obj, error=false)
 if error
  DRbRemoteError.new(obj)
 else
  DRbObject.new(obj)
 end
end

だと思います。みなさんも本の内容に沿って一緒にやってみて下さい。短いですがこのくらいで「4.2.1 can't dump」はおしまいにします。

4.2.2 DRbUndumped

では次。「4.2.2 DRbUndumped」に入ります。DRbUndumpedとは、Marshal.dumpは成功するからそのままやっちゃうと値渡しになるんだけど、参照渡しにしたい。ってときに使うもののようです。

いつも参照渡しとなるように、Marshal.dumpを失敗させるための Mixinモジュール DRbUndumped を用意しています。DRb::DRbUndumped(別名DRbUndumpedも使えます)は、クラスにincludeしたりオブジェクトにextendして使用するモジュールです。

4.2.2 DRbUndumped

こちらも本に載っている手順の通りにやってみます。簡単ですね。

foo.extend(DRbUndumped)

とすると、そのオブジェクトが参照渡しになります。また、

class Foo
 include DRbUndumped

とすると、そのクラスから生成されるオブジェクトは常に参照渡しになるようです。はい、よくわかりました。

でも、ちょっと気になりませんか。このDRbUndumpedモジュールは常にMarshal.dumpを失敗させるとのことですが、一体どんな方法で仕組みを使って失敗させているのでしょう。気になるのでDRbUndumpedのソースを見てみます。

  module DRbUndumped 
    def _dump(dummy)  # :nodoc:
      raise TypeError, 'can\'t dump'
    end
  end

これだけですか、なんでこれでMarshal.dumpできないようになるのか全然わかりません。だいたい_dumpって何?むぅぅ、、ってことでRubyのリファレンスマニュアルを見てみます。

Marshal.dump において出力するオブジェクトがメソッド `_dump' を定義している場合には、そのメソッドの結果が書き出されます。メソッド `_dump' は引数として再帰を制限するレベル limit を受け取り、オブジェクトを文字列化したものを返します。

ユーザ定義のMarshal

とありました。なるほど、Marshal.dumpの中で_dumpが必ず呼ばれるんですね。そりゃそうだろうなあ。Marshalの中を見て一応確かめてみることにします。。と、ソースを見ようと思ったらこいつはCで書かれているのか。marshal.cというソースファイルのようですね。Ruby本体のソースは全然構造や関数の用途を把握していないので全くわかんないけど。。。えーっと、_dumpはどこかなと

void
Init_marshal()
{
・・・
    s_dump = rb_intern("_dump");
・・・

いました。これですね。rb_internというのがよくわかんないけど、付近のコードを見ても定義を行なっているだけの雰囲気があるので、"_dump"という文字列に対応するような識別子がs_dumpに設定されて特定できるようになっているのでしょう多分。

じゃあ次にMarshal.dumpで呼ばれる部分にあたりをつけてみます。。と、あったあった、おそらくこの部分だと思います。

static VALUE
dump(arg)
    struct dump_call_arg *arg;
{
    w_object(arg->obj, arg->arg, arg->limit);
    if (arg->arg->dest) {
    rb_io_write(arg->arg->dest, arg->arg->str);
    rb_str_resize(arg->arg->str, 0);
    }
    return 0;
}

ここで呼ばれているw_objectの中も見てみます。この関数は、220行を超える結構長めの関数です。型などに応じて処理を分けるcaseがあるからデカいようですね。で、この中で

if (rb_respond_to(obj, s_dump)) {
    VALUE v;
    v = rb_funcall(obj, s_dump, 1, INT2NUM(limit));
・・・

としています。rb_respond_toは、おそらくRubyのrespond_to?と同じようなものでしょう。Rubyのrespond_to?は、引数の名前のメソッドを持っているかどうかを問うものです。で、rb_funcallは、名前からして処理を呼び出す(実行する)ものだと思われますから、ここでの処理は、

_dumpと言う名前のメソッドがobjにあれば、それを実行しなさい

と言うことだと思います。全て勝手な推測ばかりですが。一応_dumpを呼んでそうな気はしました。しかしRubyのソースを読めるようになるまでには多分かなりの時間がかかるだろうなあ。と思ってたら、これを見ると色々詳しくなるらしいと聞きました。が、まだ私は読めてません。


なんか今回はかなり脱線してしまいました。
ので、ちょっと纏めます。


今回のエントリのまとめ


  • Marshal.dumpできるものは値渡し、できないものは参照渡し

  • 明示的にDRbObject.newすると参照渡し

  • DRbUndumpedをくっつけたオブジェクトは参照渡し


今日のところはこれで終了です。次回は「4.3 未知のオブジェクトとDRbUnknown」に入りたいところですが、ページ数も短いのでさらっと読んだ後に「4章.参照渡しと値渡し」のまとめっぽい簡単なスクリプトを書きたいと思っています。次回で4章が終わる予定です。

2007年11月07日

4章.参照渡しと値渡し (その7)

 今回のエントリは、「4.3 未知のオブジェクトとDRbUnknown」です。前回のエントリでは4章全体のまとめも合わせて一つのエントリにすると言いましたが、(あわせると長くなりそうなので)やっぱり分けることにしました。

 はい、ではDRbUnknownって何なんでしょう、何のためにあるのでしょう。本を読みます。

DRbUnknownとは知らないクラスをMarshal.loadしてしまったときの例外を捉え、ロードできなかったオブジェクトの代わりにロードされるオブジェクトです。 DRbUnknownはロードに失敗した原因を、二つ保持しています。一つはロードに失敗したバッファ、もう一つは定義が不明なクラス名/モジュール名です。

それぞれ次のメソッドで問い合わせることができます。

DRbUnknown#buf
Marshal.loadに失敗した直列化されたオブジェクトのバッファ。
DRbUnknown#name
例外のメッセージから調べた、未知のクラス/モジュール名。
DRbUnknown#reload
もう一度Marshal.loadしてみる。

4.3 未知のオブジェクトとDRbUnknown

メモメモ。続きも読んでみます。

dRubyのライブラリは知らないクラスを受けとってしまっても、そのバッファを包んだ DRbUnknownオブジェクトを自動的に生成します。 DRbUnknownに対して元のオブジェクトのつもりでメソッドを呼ぶことはできませんが、DRbUnknownを回送することはできます。

4.3 未知のオブジェクトとDRbUnknown

 ほうほうです。Marshalを使うと、マーシャルデータからオブジェクトを復元するとき(Marshal.loadするとき)に、そのオブジェクトの型を知っていないと復元できないみたいです。dRubyではオブジェクトを渡すときにMarshalの仕組みを利用していますので、この挙動から影響を受けるんですね。

 DRbUnknownの中身をちょっとだけ見てみます。小さなクラスなので見やすいです。まずは_dumpメソッドから。

def _dump(lv) # :nodoc:
  @buf
end

_dumpはMarshalでのdump時に呼ばれるメソッドであることは前回のエントリで勉強しました。なので、DRbUnknownが別プロセスに渡されるときには、DRbUnknownが包んでいる元々のマーシャルデータが渡されます。で、受け取ったプロセスで復元できたらそれでよし、復元できなければそこでもDRbUnknownになります。ってことですね。

 ちなみに、@nameを設定している部分はここです。

def initialize(err, buf)
  case err.to_s
  when /uninitialized constant (\S+)/
@name = $1
  when /undefined class\/module (\S+)/
@name = $1
  else
@name = nil
  end
  @buf = buf
end

 例外の文字列から名前を取っているみたいです。$1ってのは、正規表現にマッチした最初の文字列が入ってる組み込み変数です。@nameがnilになることもあるのか。elseに入るのはどういうときなのかなあ、名前すらわからないエラーのときですよねきっと。多分marchal.cの


static VALUE
r_object0(arg, proc, ivp, extmod)
struct load_arg *arg;
VALUE proc;
int *ivp;
VALUE extmod;
{
・・・

のメソッドあたりで投げられている例外の大半が該当するのだと思います。実際にDRbUnknownが捕捉している例外の文字列はvariable.cから出るやつのようです。中身はややこしそうなので追わないことにしました。このくらいにして本を読み進めます。

DRbUnknownの機構によって、クラス定義を知らないオブジェクトを(メソッド呼び出しはできないが)保持しておくことが可能になります。

プロセス間でオブジェクトを交換するためのQueueを考えてみましょう。中継するQueueのサービスがpushされる可能性のある全てのクラス定義を事前に知らなくてはならないのは、難しいことがあります。 DRbUnknownはこういった局面で特に有用な機能です。

4.3 未知のオブジェクトとDRbUnknown

 本に書かれていることはよくわかります。プロセス間通信を行う上では、サービス側が全てのオブジェクトの型を知っておくことは非現実的であったり面倒であったりすることが多いです。なので、dRubyでは、マーシャルデータからオブジェクトを復元することが出来なくても、それを保持できる仕組みを提供しています。それがDRbUnknownです。

 ちょっとかなり端折り気味ですが、以下にまとめを書いてDRbUnknownは終わりにします。一度は本の内容にそって実験してみて下さいね。

DRbUnknownのまとめ


  • DRbUnknownオブジェクトはMarshal.loadが失敗したときに生成される

  • DRbUnknownオブジェクトは、オブジェクトに復元できなかったマーシャルデータの情報を保持する

  • DRbUnknownオブジェクトの状態では、(中身のオブジェクトの)メソッドを呼べない

  • DRbUnknownオブジェクトをdumpすると、包み込んでいるマーシャルデータがそのまま返される

  •  つまりdRubyでのプロセス間通信時には、気にせずそのままホイホイ渡せばいい
  • Marshal.loadが成功したら、DRbUnknownオブジェクトの中身のオブジェクトが取れる


次回こそ、4章を終わりにします。

2007年11月14日

4章.参照渡しと値渡し (その8)

今回は4章のおさらいとして、observer(observer.rb)で遊ぼうと思います。
observerってのは、Rubyに標準で同梱されているライブラリです。これはRubyでオブジェクト指向プログラミングのデザインパターンのひとつであるObserverパターンの概念を、簡単に実装できるようにしてくれるもののようです。このソースのコメントに書かれているexampleのスクリプトをいじってみることにします。exampleは以下の通りです。

require "observer"

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def run
  lastPrice = nil
  loop do
   price = Price.fetch(@symbol)
   print "Current price: #{price}\n"
   if price != lastPrice
    changed         # notify observers
    lastPrice = price
    notify_observers(Time.now, price)
   end
   sleep 1
  end
 end
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

class Warner     ### An abstract observer of Ticker objects.
 def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = Ticker.new("MSFT")
WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
ticker.run

簡単に解説します。
Tickerがイベント発生機です。ランダムに金額をfetchし、それをObserverに通知します。通知するためのメソッドがObservableモジュールに定義されていますので、それをincludeしておきます。
Tickerが発生したイベントは、各Observerに通知されます。通知されたイベントに反応するかどうかの判断は各Observer任せです。
WarnHighやWarnLowなどのクラスは、インスタンス生成時(initialize時)に、自分自身をObserverとしてTickerに登録します。

実行してみましょう。

Current price: 83
Current price: 75
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
Current price: 90
Current price: 134
+++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
Current price: 134
Current price: 112
Current price: 79
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79
・・・

こんな感じに延々と出力されるはずです。

はい、ではこれにdRubyを使ってみます。ObservableとObserverのプロセスを別にして、プロセス間通信してみましょう。まずは何も考えず安直に作ってみます。わかりにくいので、各プロセスに、それぞれ便宜的に「Observable側」と「Observer側」という名前をつけることにしますね。

[Observable側]

require "observer"
require 'drb/drb'

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def run
  lastPrice = nil
  loop do
   price = Price.fetch(@symbol)
   print "Current price: #{price}\n"
   if price != lastPrice
    changed         # notify observers
    lastPrice = price
    notify_observers(Time.now, price)
   end
   sleep 1
  end
 end
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

class Warner     ### An abstract observer of Ticker objects.
 def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = Ticker.new("MSFT")
DRb.start_service('druby://[ホスト]:9191', ticker)
ticker.run

[Observer側]

require 'drb/drb'

class Warner     ### An abstract observer of Ticker objects.
def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = DRbObject.new_with_uri('druby://[ホスト]:9191')
low = WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)

スクリプトのダウンロード
[Observable側] 1-1.rb
[Observer側] 1-2.rb


 とりあえずできたかな。ではこれで動かしてみます。まずObservable側を動かしてから、次はObserver側を動かします。無事動きました!Observable側のコンソールにObserver側から登録したオブジェクトの出力メッセージが出ています。うまくいきました。

 ・・・でも、よく考えると微妙です。まず、クラスの定義がどちらのプロセスにも重複して書いてあります。これを別のファイルにしてrequireしたとしても、Observable側が、扱うクラスをすべて知っておかなければならないって言うのは(Javaなどの型縛り言語なら当然だとしても)ちょっとRubyやdRubyの考え方からは外れるような気がします。あと、当然ですがObserver側はすぐ終了しちゃいますね。。で、実行結果のメッセージはすべてObservable側に出ている。で、今までに勉強したとおりdRubyは基本的に値渡しだから、WarnLowやWarnHighは、Observer側のインスタンスじゃなくて、それを複製したオブジェクトをObservable側が持ってしまってるって事です。これはまずいですね。Observer側のプロセスが持っているオブジェクトの内容を参照してもらわないと色々な場面で不便そうです。
そこで、


  • Observable側が、扱うクラスを知ってないと駄目なのはだるそう

  • Observable側にObserverのインスタンスが値渡しされているのは不便そう

の、この2点を解決してみたいと思います。じゃあ、まず、単純にObservable側から、Warnerクラスとその派生クラスたちの定義を消して動かしてみます。Observer側は変えませんよ。

[Observable側]

require "observer"
require 'drb/drb'

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def run
  lastPrice = nil
  loop do
   price = Price.fetch(@symbol)
   print "Current price: #{price}\n"
   if price != lastPrice
    changed         # notify observers
    lastPrice = price
    notify_observers(Time.now, price)
   end
   sleep 1
  end
 end
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

ticker = Ticker.new("MSFT")
DRb.start_service('druby://[ホスト]:9191', ticker)
ticker.run

スクリプトのダウンロード
[Observable側] 2-1.rb


では、動かします。・・・っと、あれれ?

(druby://[ホスト]:9191) ・・・observer.rb:126 :in `add_observer': observer needs to respond to `update' (NoMethodError)

エラーになっちゃいました。observer.rbのエラーになっている箇所を見てみます。

module Observable

 #
 # Add +observer+ as an observer on this object. +observer+ will now receive
 # notifications.
 #
 def add_observer(observer)
  @observer_peers = [] unless defined? @observer_peers
  unless observer.respond_to? :update
   raise NoMethodError, "observer needs to respond to `update'"
  end
  @observer_peers.push observer
 end

うーん、respond_to?で聞いた結果、updateってメソッドがないって言われています。なぜでしょう??仕方ないので、ここで、observerの中身を見てみます。ブレークポイントを置いたりしてみましょう。

<DRb::DRbUnknown:0x29a3e5c @buf="\004\bo:\fWarnLow\006:\v@limitiU", @name="WarnLow">

あらら、DRbUnknownってオブジェクトになっていますね。これは前回のエントリで学んだやつです。そうなのか、なるほど、そうですね、考えてみれば当然ですね。WarnLowなどのクラスをObservable側のプロセスは知りません。自分の知らないものが値渡しとして渡されてくると、Marshal.load時に失敗するので駄目ってことですね。なるほど、結局参照渡しにしないと駄目ってことか。では、Observer側を参照渡しに変えてみます。

[Observer側]

require 'drb/drb'

DRb.start_service
class Warner     ### An abstract observer of Ticker objects.
include DRbUndumped
def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = DRbObject.new_with_uri('druby://[ホスト]:9191')
low = WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
DRb.thread.join

スクリプトのダウンロード
[Observer側] 2-2.rb

3行変えました。


  • 参照渡しなので、DRb.start_serviceする

  • 参照渡しなので、DRbUndumpedをincludeする

  • 実体がこっちのプロセスにあるため、死ぬとまずいのでDRb.thread.joinで待機

これで実行しましょう・・・うまくいきました!メッセージもちゃんとObserver側に出ていますね!めでたしめでたし。なんとなく参照渡しと値渡しの感覚がつかめたような気がします。皆さんもRubyのライブラリをdRubyにしてみたりして遊んでみて下さい。結構簡単に出来ちゃうことに驚くと思います。

これで4章は終了です。

About 4章

ブログ「dRubyの正面」のカテゴリ「4章」に投稿されたすべてのエントリーのアーカイブのページです。過去のものから新しいものへ順番に並んでいます。

前のカテゴリは2章です。

次のカテゴリはその他です。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。