« 2007年10月 | メイン

2007年11月 アーカイブ

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 2007年11月

2007年11月にブログ「dRubyの正面」に投稿されたすべてのエントリーです。過去のものから新しいものへ順番に並んでいます。

前のアーカイブは2007年10月です。

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