teamon.eu

blah blah blah

Ruby - ! poprzez method_missing

07 maja 2009

Kategorie:

Tagi:

  • Ruby

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 przyykladu
  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.

8 komentarzy

  • Seban 07 maja 2009 10:57:41

    Ciekawe jak te wyniki się zmieniają, gdy w method_missing obsługuje więcej brakujących metod niż tylko te pasujące do /(.+)!$/

  •  demikaze 07 maja 2009 14:12:28

    No właśnie, każda taka ciekawostka, każde wykorzystanie właśnie tych mechanizmów których z różnych powodów nie ma i nie da się uzyskać w innych językach dynamicznych a tym bardziej statycznych, to kolejne obciążenie dla i tak już mocno naciągniętej szeroko rozumianej wydajności - z jednej strony ciekawe te możliwości, ale z drugiej strony mam coraz bardziej wrażenie że im większa "rzecz" do zrobienia w takim języku tym bardziej te możliwości są "tylko" ciekawe :)

    TAK - jestem chyba raczej zwolennikiem języków statycznych, choć nie doszukiwałbym się w tym jakiegoś fanatyzmu, co potwierdza ostatni czas spędzony na drążeniu tematu dynamizmu w dzisiejszych językach, teoria przeplatana z praktyką, na zasadzie "a może to nie jest takie do końca złe jak mi się wydawało", ale mając raczej ogromne doświadczenie z językami statycznymi (w sensie ilości wklepanego, debugowanego i utrzymywanego kodu, ilości ukończonych w ten sposób projektów) coraz bardziej "czuję" że w skrypcikach to owszem ma sens, bo szybki cykl pisanie-uruchomienie-poprawienie, sprytne w tym sensie, ale w czymś większym i zwłaszcza czymś co miałoby wyłamać się poza ramy niewielkich RIA to raczej nie wróżę świetlanej przyszłości :)

    to takie przydługie przemyślenia na szybko

  • teamon 07 maja 2009 14:26:53

    @demikaze Merb wykorzystuje to rozsądnie i jakoś to bardzo nie przeszkadza. Uaktualniłem wpis o definiowanie "na sztywno", możesz porównać wyniki.

  • Tubis 10 maja 2009 13:39:17

    Dodam tylko że nie trzeba do class_eval przekazywać (IMHO) brzydkiego łańcucha znaków, a można to zrobic przez ładny blok:

    self.class.class_eval do
    define_method desired_method do |*args, &block|
    self.replace self.send($1, *args, &block)
    end
    end

  • teamon 10 maja 2009 13:40:55

    @Tubis a sprawdzałeś? ;]
    SyntaxError: compile error
    (irb):1: syntax error, unexpected ',', expecting '|'
    define_method(:meth) do |*args, &block|

  • Tubis 10 maja 2009 15:49:49

    Dziwne bo to:
    http://pastie.org/473657
    Działa na moim ruby 1.9.1 bez problemu

  • Tubis 10 maja 2009 15:52:29

    http://radarek.jogger.pl/2008/12/12/nowosci-i-zmiany-w-ruby-1-9-5-bloki-domkniecia-nowa-lambda/

    Ruby 1.9.1 pójdzie mój kod.
    Ruby 1.8 nie :)

  • teamon 10 maja 2009 16:05:24

    No właśnie ;]

Zostaw komentarz

code