Ruby - ! poprzez method_missing

Dla tych co nie wiedzą co to method_missing i co chodzi z ! polecam posty na blogu radarka: Ruby a metody z ‘?’ i ‘!’ w nazwie oraz method_missing w Rubym - nie pomiń niczego!.

Załóżmy, że piszemy bibliotekę dodającą sporo metod do klasy String i chcielibyśmy, żeby każda metoda miała swój odpowiednik zakończony !.

Można to zrobić w taki sposób:

class String
  def plural
    self !~ /s$/ ? self + "s" : self # tylko dla przykładu
  end

  def plural!
    self.replace(plural)
  end

  def foo
    ...
  end

  def foo!
    self.replace(foo)
  end
end

Ale jest to co najmniej średnio wygodne.

Z pomocą przychodzi method_missing. W bardzo łatwy sposób można zdefiniować regułę, która wyłapie odwołanie do nieistniejącej metody zakończonej !.

class String
  # dla przykładu, to nie jest idealna implementacja :P
  def plural
    self !~ /s$/ ? self + "s" : self
  end
end


class String
  def method_missing(method, *args, &block)
    if method.to_s =~ /(.+)!$/ && respond_to?($1)
      self.class.class_eval <<-EOF
        def #{method}(*args, &block)
          replace send(:#{$1}, *args, &block)
        end
      EOF
      self.send(method, *args, &block)
    else
      super
    end
  end
end

c = "cat"
c.plural # => "cats"
c # => "cat"
c.plural! # => "cats"
c # => "cats"

d = "dog"
d.plural! # => "dogs"
d # => "dogs"

W tym przykładzie method_missing sprawdza czy jest dostępna metoda plural, a następnie definiuje metode plural!. Niektórzy pewnie zapytają: “a co robi tam ten eval? po co to?”. Już wyjaśniam. Poprzez zdefiniowanie methody plural! gdy następnym razem wywołamy c.plural! metoda method_missing nie zostanie wywołana, ponieważ metoda plural! już istnieje. No tak, ale to przecież bez różnicy, działa tak samo, prawda? Okazuje się, że jednak jest różnica…

Prosty benchmark wszystko dobitnie pokazuje:

require 'benchmark'

class String
  def plural
    self !~ /s$/ ? self + "s" : self
  end
end

Benchmark.bm do |b|
  n = 1000000
  b.report("defined plural!") {
    class String
      def plural!
        replace plural
      end
    end

    s = "cat"
    n.times { s.plural! }
  }

  String.send(:undef_method, :plural!)

  b.report("not defining") {
    class String
      def method_missing(method, *args, &block)
        if method.to_s =~ /(.+)!$/ && respond_to?($1)
          replace send($1, *args, &block)
        else
          super
        end
      end
    end

    s = "cat"
    n.times { s.plural! }
  }

  b.report("defining") {
    class String
      def method_missing(method, *args, &block)
        if method.to_s =~ /(.+)!$/ && respond_to?($1)
          self.class.class_eval <<-EOF
            def #{method}(*args, &block)
              replace send(:#{$1}, *args, &block)
            end
          EOF
          self.send(method, *args, &block)
        else
          super
        end
      end
    end

    s = "cat"
    n.times { s.plural! }
  }
end

A oto wyniki:

[teamon ~/Desktop] ruby1.9 str_bench.rb
                  user      system    total     real
defined plural!   2.280000  0.010000  2.290000  ( 2.311500)
method_missing    6.140000  0.030000  6.170000  ( 6.215187)
combo             2.450000  0.020000  2.470000  ( 2.492664)

[teamon ~/Desktop] ruby str_bench.rb
                  user      system    total     real
defined plural!   1.710000  0.010000  1.720000  ( 1.734700)
method_missing    6.060000  0.020000  6.080000  ( 6.137054)
combo             2.700000  0.010000  2.710000  ( 2.737030)

[teamon ~/Desktop] jruby str_bench.rb
                  user      system    total     real
defined plural!   1.229000  0.000000  1.229000  ( 1.203000)
method_missing    5.874000  0.000000  5.874000  ( 5.874000)
combo             1.723000  0.000000  1.723000  ( 1.724000)


# jakby co:
[teamon ~/Desktop] ruby -v
ruby 1.8.6 (2008-03-03 patchlevel 114) [universal-darwin9.0]

[teamon ~/Desktop] ruby1.9 -v
ruby 1.9.1p0 (2009-01-30 revision 21907) [i386-darwin9.6.0]

[teamon ~/Desktop] jruby -v
jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-31 rev 6586) [i386-java]

Jak widać dla ruby1.9 i jruby różnica między zdefiniowaniem “na sztywno” plural! jest niewielka (dla 1.8.6 jest już trochę więcej). Łatwo jednak zauważyć, że używanie samego method_missing bez definiowania metody znacznie odstaje wydajnościowo.

Swoją drogą trochę mnie dziwi, że 1.9 okazuje się wolniejsze od 1.8.6.


Looking for comments section?

Send me an email instead to teamon@me.com