2008-01-29 (Tue) [長年日記]

_ Rails 2.0 の ActiveSupport の 1.second とかが地味に変わっている件 はてなブックマークに追加 del.icio.usに追加 MM/Memoに追加

Rails 1.2.x の場合

$ ruby script/console
>> 1.second
=> 1
>> 1.second.class
=> Fixnum

Rails 2.0.x の場合

$ ruby script/console
>> 1.second
=> 1 second  # ん? Fixnum じゃなくなったのか!?
>> 1.second.class
=> Fixnum  # え! Fixnum なんだ???

というわけで、解析ですが、 その前にまずは Rails 1.2.x 系だとどういう定義 だったかを見てみましょう。

Rails 1.2.x のおさらい

module Time
  def seconds
    self
  end

というのが定義されていて、

class Numeric #:nodoc:
  include ActiveSupport::CoreExtensions::Numeric::Time 
  include ActiveSupport::CoreExtensions::Numeric::Bytes 
end

という形で Numeric クラスに include されているだけ、 実にシンプルですね。

Rails 2.0.x の解析

そして、Rails 2.0.x の解析です。

結論から言うと、「委譲」によって実現されているという ことになります。

まずは、second の定義を見てみます。

active_support/core_ext/numeric/time.rb

def seconds
  ActiveSupport::Duration.new(self, [[:seconds, self]])
end

どうやら、1.second で返って来ているのは、 Duration というクラスのオブジェクトらしい。

active_support/duration.rb

class Duration < BasicObject
  attr_accessor :value, :parts

  def initialize(value, parts) #:nodoc:
    @value, @parts = value, parts
  end

  ...

 end

で、1.second のレシーバである「1」が @value に入り、 [[:seconds, self]] という配列の配列が @parts に入る。

あと、BasicObject というのを継承していることにも注目。 こいつは実際には、BlankSlate というクラスでして。

active_support/vendor/builder-2.1.2/blankslate.rb

######################################################################
# BlankSlate provides an abstract base class with no predefined
# methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
# BlankSlate is useful as a base class when writing classes that
# depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
#
class BlankSlate
  class << self

    # Hide the method named +name+ in the BlankSlate class.  Don't
    # hide +instance_eval+ or any method beginning with "__".
    def hide(name)
      if instance_methods.include?(name.to_s) and
        name !~ /^(__|instance_eval)/
        @hidden_methods ||= {}
        @hidden_methods[name.to_sym] = instance_method(name)
        undef_method name
      end
    end
...中略...
  end

  instance_methods.each { |m| hide(m) }
end

ってな感じで、__send__, __id__, instance_eval 以外の 全てのメソッドを undef しちゃう hide という クラスメソッドが定義されていて、class 定義の最後に、 instance_methods.each {} によって、全てのメソッドに 対して hide を実行します。

で、いったい何のためにそんなことをしているのかは、 Ruby1.9で追加されたBasicObjectの 解説 を読むと、何のためのクラスなのかがわかると思います。

上のコメントにも書いてありますが、

# BlankSlate is useful as a base class when writing classes that
# depend upon <tt>method_missing</tt> (e.g. dynamic proxies).

ってことですね。

というわけで、

class Duration < BasicObject

ってクラスは、__send__, __id__, instance_eval しか 存在しない BasicObject クラスを継承して作られている、 というわけです。

これでやっと、最初の疑問、「.inspect すると 1 second という 文字列が返ってくるのに、.class するとなぜ Fixnum が 返ってくるか」がわかります。

.inspect は、class Duration を読んでもらうとして。

で、.class が呼ばれても、このクラスには .class という メソッドはないわけです。hide しちゃったから。

なので、method_missing が発動されます。

で、class Duration の method_missing がどうなっているか というと、こんな感じ。

def method_missing(method, *args, &block) #:nodoc:
  value.send(method, *args)
end

value すなわち、1.second の 1 に対して、method すなわち .class を *args(この場合は空ですね .class なので)と共に send する、 つまり 1 に委譲して、結局

1.class

が実行されて、

=> Fixnum

となるわけです。

Rails1.2.x の頃のように Fixnum そのままでもそんなに 困ってはいなかったんだが、すごいこだわりですねw

関連記事

Permalink | このエントリを含むはてなブックマーク | このエントリをはてなブックマークに追加 | このエントリを含むMM/Memo | このエントリをMM/Memoに追加 | このエントリを含むdel.icio.us | このエントリをdel.icio.usに追加 | Tags: ruby rails
[]