vieweditattachhistoryswikistopchangessearchhelp

Ruby で、それほどお手軽ではないプロトタイプベース的 bankAccount

Ruby でお手軽にプロトタイプベース的 BankAccount」で、クラスをオブジェクトとして用いることで醸し出される「お手軽感」に対して疑問が呈されましたので、実際に、クラスをオブジェクトして使わずに、インスタンスとその特異メソッドを使って、Ruby でプロトタイプベース的な bankAccount を作ったらどうなるかを試してみましょう。

クラスをオブジェクトとして使った場合、そのクラスメソッド(クラスの特異メソッド)はクラスの継承関係にのっとって、サブクラスから起動することができます。このからくりは、
ことに加えて、
という Ruby のクラスとその特異クラス、特異メソッドの(インスタンスの特異クラス、特異メソッドとは異なる)特殊な振る舞いによって、実現されています。

一方でこのことは、インスタンスで、クラスをオブジェクトして使ってプロトタイプベース的なことを実現したのと同じようなことをしようとすると、
という仕事が増えることを意味します。なんかいろいろと面倒そうですね。これをして私は、同じことをするのにもプロトタイプベースのオブジェクトとしてクラスを使った方が「お手軽」だと言っているわけです。実際にどんなふうに面倒になるか見てみましょう。まず、モジュールを使わずに書いてみると…
class Prototype
  attr :proto, true
  def method_missing(sel,*args); self.proto.send(sel,*args) end
  def klone
    obj=Prototype.new
    obj.proto=self
    obj
  end
end

bankAccount=Prototype.new
def bankAccount.get_dollars(me)
  class << me; attr :dollars, true end
  me.dollars
end

def bankAccount.set_dollars(me,x)
  me.get_dollars(me)
  me.dollars=x
end

def bankAccount.deposit(me,x)
  me.set_dollars(me,me.get_dollars(me)+x)
end

def bankAccount.withdraw(me,x)
  me.set_dollars(me,[0,me.get_dollars(me)-x].max)
end

bankAccount.set_dollars(bankAccount,200)

bankAccount.get_dollars(bankAccount)
=> 200
bankAccount.deposit(bankAccount,50)
=> 250
bankAccount.withdraw(bankAccount,100)
=> 150
bankAccount.withdraw(bankAccount,200)
=> 0

myAccount=bankAccount.klone
myAccount.set_dollars(myAccount,100)
=> 100
myAccount.deposit(myAccount,400)
=> 500
bankAccount.get_dollars(bankAccount)
=> 0

stockAccount=bankAccount.klone

def stockAccount.get_numShares(me)
  class << me; attr :numShares, true end
  me.numShares
end

def stockAccount.set_numShares(me,x)
  me.get_numShares(me)
  me.numShares=x
end

def stockAccount.get_pricePerShare(me)
  class << me; attr :pricePerShare, true end
  me.pricePerShare
end

def stockAccount.set_pricePerShare(me,x)
  me.get_pricePerShare(me)
  me.pricePerShare=x
end

def stockAccount.get_dollars(me)
  me.get_numShares(me)*me.get_pricePerShare(me)
end

def stockAccount.set_dollars(me,x)
  me.set_numShares(me,Float(x)/me.get_pricePerShare(me))
  me.get_dollars(me)
end

stockAccount.set_numShares(stockAccount,10)
stockAccount.set_pricePerShare(stockAccount,30)

stockAccount.get_dollars(stockAccount)
=> 300

stockAccount.set_dollars(stockAccount,150)
=> 150.0
stockAccount.get_numShares(stockAccount)
=> 5.0

myStock=stockAccount.klone
myStock.set_numShares(myStock,10)
myStock.set_pricePerShare(myStock,30)
myStock.set_dollars(myStock,600)
=> 600

myStock.get_numShares(myStock)
=> 20.0
myStock.deposit(myStock,60)
=> 660.0
myStock.get_numShares(myStock)
=> 22.0
myStock.withdraw(myStock,120)
=> 540.0
myStock.get_numShares(myStock)
=> 18.0

def bankAccount.deposit(me,x); me.set_dollars(me,me.get_dollars(me)+x*0.9) end

bankAccount.deposit(bankAccount,100)
==> 90.0    # 0 + 100 * 0.9
myAccount.deposit(myAccount,100)
==> 590.0   # 500 + 100 * 0.9
stockAccount.deposit(stockAccount,100)
==> 240.0   # 150 + 100 * 0.9
myStock.deposit(myStock,100)
==> 630.0   # 540 + 100 * 0.9
というような感じになります(Ruby 1.6.7 + irb で動作確認をしています)。

明示的に設けられた、プロトタイプを束縛するための proto スロットと、それを参照して行なわれる委譲ロジックは、Prototype クラスに定義してあります。この Prototype のインスタンスをプロトタイプベースのオブジェクトとしてスクリプティングを行ないました。

特異メソッドを呼ぶときに、第一パラメータにレシーバを必ず入れています。これは、継承関係にあるメソッドを起動したとき、そのメソッドのコンテキストで self がレシーバを束縛するのをシミュレートするためのものです。実際の self はというと、Prototype#method_missingで定義したとおり、この文脈にしたがってメソッドホルダ(のインスタンス)を束縛しています。期待されるのは(委譲前の)レシーバなので、これを第一パラメータとしていちいち渡し、me に束縛して self 代わりに使っています。この手法は、Python のそれに似ていなくもありませんね。
--sumim


UnboundMethod#bind を使った例(NG)

まあ、こういうことはまずしませんが、Smalltalk/Squeak では、
| method |
method := Number compiledMethodAt: #+.
3 withArgs: {4} executeMethod: method   "==> Error: My subclass should have overridden #''"
のようにして比較的自由に、任意のコンパイル済みメソッドを任意のオブジェクトにバインドして評価することができます(コンパイル済みのメソッドを、メッセージ送信を介さずに、まるで通常の言語における関数のように起動しているわけですね)。この例では、抽象クラスである Number に定義されている #+ (オーバーライドするよう勧告のための例外を出すコード)を SmallInteger の 3 にバインドして評価し、わざとエラーを出させています。

Ruby でもこれに似たようなことはできて、たとえば、
3.method(:+).unbind.bind(4).call(5)
=> 9
というようなことができます。つまり、3 にバインドバインドした + というメソッドをオブジェクトして取り出して、それをいったん、誰にもバインドされていない UnboundMethod にし、改めて 4 とバインドして評価する、というようなことです。

この方法を使用して Prototype#method_missing のところで、移譲先のメソッドを起動することがができれば、パラメータが増えて鬱陶しい self のシミュレーションをもっとスマートにできるはずです。ところが Ruby では型チェックが厳しく、アンバウンドしたメソッドにバインドできるのは、同じクラスのオブジェクトに限られ、さらに本目的として用いるには致命的なことに、特異メソッドをこの方法で起動することはできない、という制約が課せられています(まあ仕組みを知れば、第一のルールが適用されるのでこの結果は当たり前ですが)。

では、特異メソッドをあきらめて(この時点で Ruby でこれをする意味は半減してしまうのですが、それでもいいので)通常のメソッドではどうか、とも思ったのですが再バインドできるのは、同じクラスのインスタンスに限られるようでこれも残念ながら実現できませんでした。--sumim


Delegate モジュールを使った例

このページを編集 (6836 bytes)


Congratulations! 以下の 4 ページから参照されています。

This page has been visited 4109 times.