Method_Missing Gotchas in Ruby

method_missing gotchas in Ruby

A somewhat obvious one: always redefine respond_to? if you redefine method_missing. If method_missing(:sym) works, respond_to?(:sym) should always return true. There are many libraries that rely on this.

Later:

An example:

# Wrap a Foo; don't expose the internal guts.
# Pass any method that starts with 'a' on to the
# Foo.
class FooWrapper
def initialize(foo)
@foo = foo
end
def some_method_that_doesnt_start_with_a
'bar'
end
def a_method_that_does_start_with_a
'baz'
end
def respond_to?(sym, include_private = false)
pass_sym_to_foo?(sym) || super(sym, include_private)
end
def method_missing(sym, *args, &block)
return foo.call(sym, *args, &block) if pass_sym_to_foo?(sym)
super(sym, *args, &block)
end
private
def pass_sym_to_foo?(sym)
sym.to_s =~ /^a/ && @foo.respond_to?(sym)
end
end

class Foo
def argh
'argh'
end
def blech
'blech'
end
end

w = FooWrapper.new(Foo.new)

w.respond_to?(:some_method_that_doesnt_start_with_a)
# => true
w.some_method_that_doesnt_start_with_a
# => 'bar'

w.respond_to?(:a_method_that_does_start_with_a)
# => true
w.a_method_that_does_start_with_a
# => 'baz'

w.respond_to?(:argh)
# => true
w.argh
# => 'argh'

w.respond_to?(:blech)
# => false
w.blech
# NoMethodError

w.respond_to?(:glem!)
# => false
w.glem!
# NoMethodError

w.respond_to?(:apples?)
w.apples?
# NoMethodError

What are the Ruby Gotchas a newbie should be warned about?

Wikipedia Ruby gotchas

From the article:

  • Names which begin with a capital letter are treated as constants, so local variables should begin with a lowercase letter.
  • The characters $ and @ do not indicate variable data type as in Perl, but rather function as scope resolution operators.
  • To denote floating point numbers, one must follow with a zero digit (99.0) or an explicit conversion (99.to_f). It is insufficient to append a dot (99.), because numbers are susceptible to method syntax.
  • Boolean evaluation of non-boolean data is strict: 0, "" and [] are all evaluated to true. In C, the expression 0 ? 1 : 0 evaluates to 0 (i.e. false). In Ruby, however, it yields 1, as all numbers evaluate to true; only nil and false evaluate to false. A corollary to this rule is that Ruby methods by convention — for example, regular-expression searches — return numbers, strings, lists, or other non-false values on success, but nil on failure (e.g., mismatch). This convention is also used in Smalltalk, where only the special objects true and false can be used in a boolean expression.
  • Versions prior to 1.9 lack a character data type (compare to C, which provides type char for characters). This may cause surprises when slicing strings: "abc"[0] yields 97 (an integer, representing the ASCII code of the first character in the string); to obtain "a" use "abc"[0,1] (a substring of length 1) or "abc"[0].chr.
  • The notation statement until expression, unlike other languages' equivalent statements (e.g. do { statement } while (not(expression)); in C/C++/...), actually never runs the statement if the expression is already true. This is because statement until expression is actually syntactic sugar over

    until expression
    statement
    end

    , the equivalent of which in C/C++ is while (not(expression)) statement; just like statement if expression is an equivalent to

    if expression
    statement
    end

    However, the notation

    begin
    statement
    end until expression

    in Ruby will in fact run the statement once even if the expression is already true.

  • Because constants are references to objects, changing what a constant refers to generates a warning, but modifying the object itself does not. For example, Greeting << " world!" if Greeting == "Hello" does not generate an error or warning. This is similar to final variables in Java, but Ruby does also have the functionality to "freeze" an object, unlike Java.

Some features which differ notably from other languages:

  • The usual operators for conditional expressions, and and or, do not follow the normal rules of precedence: and does not bind tighter than or. Ruby also has expression operators || and && which work as expected.

  • def inside def doesn't do what a Python programmer might expect:

    def a_method
    x = 7
    def print_x; puts x end
    print_x
    end

    This gives an error about x not being defined. You need to use a Proc.

Language features

  • Omission of parentheses around method arguments may lead to unexpected results if the methods take multiple parameters. The Ruby developers have stated that omission of parentheses on multi-parameter methods may be disallowed in future Ruby versions; the current (November 2007) Ruby interpreter throws a warning which encourages the writer not to omit (), to avoid ambiguous meaning of code. Not using () is still common practice, and can be especially nice to use Ruby as a human readable domain-specific programming language itself, along with the method called method_missing().

Ruby any way to catch messages before method_missing?

Not that I know of.

The most performant bet is usually to use method_missing to dynamically add the method being to a called to the class so that the overhead is only ever incurred once. From then on it calls the method like any other method.

Such as:

class Foo
def method_missing(name, str)

# log something out when we call method_missing so we know it only happens once
puts "Defining method named: #{name}"

# Define the new instance method
self.class.class_eval <<-CODE
def #{name}(arg1)
puts 'you passed in: ' + arg1.to_s
end
CODE

# Run the instance method we just created to return the value on this first run
send name, str
end
end

# See if it works
f = Foo.new
f.echo_string 'wtf'
f.echo_string 'hello'
f.echo_string 'yay!'

Which spits out this when run:

Defining method named: echo_string
you passed in: wtf
you passed in: hello
you passed in: yay!

method_missing Within instance_eval

The expression foo = "" will never be a method call. It is a local variable assignment. This is a fact of Ruby's syntax. In order to call a setter, you have to specify a receiver explicitly. This is why most Ruby pseudo-DSLs use the Dwemthy-style:

class Dragon < Creature
life 1340 # tough scales
strength 451 # bristling veins
charisma 1020 # toothy smile
weapon 939 # fire breath
end

That avoids the equals sign problem.

Ruby: Catching All Methods Sent to an Object

As noted in the comments and the answer from @DanCheail, if you're using ruby 1.9+ you should use a BasicObject rather than undefining methods of Object. At the time of this posting usage of 1.8.x was still quite prevalent, even though 1.9 had been released for some time.


Rather than attempting to "redefine" anything, you could wrap your object in a blanked (for the most part) proxy object, something like:

class MyProxy
instance_methods.each do |meth|
# skipping undef of methods that "may cause serious problems"
undef_method(meth) if meth !~ /^(__|object_id)/
end

def initialize(object)
@object = object
end

def method_missing(*args)
# do stuff
p args
# ...
@object.send(*args)
end
end

MyProxy.new(MyClass.new).whatever

Method_missing not running when it should

There are a number of problems with your code. Let's go through one by one.

From the documentation,

method_missing(*args) private
Invoked by Ruby when obj is sent a message it cannot handle.

Here message refers to the method. In ruby, whenever you're calling a method on an object, you're actually sending a message to the object

To better understand this, try this in the irb shell.

1+2
=> 3
1.send(:+,2)
=> 3

Here 1 and 2 are objects of Fixnum class. You can confirm that by using 1.class. Ok, back to your question. So, a method_missing method should be called on an instance.

team = Team.new
team.hawks

If you try the above piece of code, you'll get an error saying 'fetch': key not found: :roster (KeyError)

You can get around this by passing a default value as the second parameter to fetch method. Replace your initialize method with

def initialize(stats = {})
@cust_roster = stats.fetch(:roster, [])
@cust_total_per = stats.fetch(:per, 0)
@cust_name = stats.fetch(:name, "anon")
@cust_best_player = stats.fetch(:best, "anon")
@@teams << self

end

If you execute the script, you'll get a stack level too deep (SystemStackError) because of a small typo in this line.

str = methID.id2name

In the method definition, you're receiving an argument with the name of methId but inside you're trying to call methID. Fix it with

str = methId.id2name

If you execute your script, you'll again get an error saying undefined method uppercase for "hawks":String (NoMethodError)

This is because there is no uppercase method on strings. You should instead use the upcase method.

Team.new(roster:[], per: 0, name: str.upcase, best: 0)

and you should be good to go.

For more, see http://apidock.com/ruby/BasicObject/method_missing

Hope this helps!

Ruby: why does puts call to_ary?

puts is a synonym for $stdout.puts. $stdout is an IO class, so look at the documentation for IO.puts:

Writes the given objects to ios as with IO#print. Writes a record
separator (typically a newline) after any that do not already end with
a newline sequence. If called with an array argument, writes each
element on a new line.

This mean that puts method is intended to write several lines of output. Thus it tries to call to_ary method on an object and if to_ary is defined, then prints each element of the returned Array on a new line, else puts calls to_s method.

to_ary internal usage is really not well documented in the Ruby documentation (Matz points this out in his The Ruby Programming Language book).

Methods print and p on the other hand don't call to_ary, only to_s.

Sidenote: Interesting, that to_ary must return real Array object, not an object defining each method or something else:

class Test
def to_ary
10.downto(1)
end
end

puts Test.new

#TypeError: can't convert Test to Array (Test#to_ary gives Enumerator)
# from (irb):28:in `puts'
# from (irb):28:in `puts'
# from (irb):28

When might a dispatch table be as good as method_missing in Ruby?

If you are frequently calling something caught by method_missing, the act of searching for a nonexistent method may end up being a performance issue in your code. So a dispatch table might be one way of working around that.

Here's a good article describing the performance issues of method_missing and attempts at optimizing them: http://www.alef1.org/ruby/method_missing/

How to find out what is intercepting 'method_missing'

I think @Sebi's answer is helpful, but I'd like to improve it like this:

set_trace_func proc { |event, file, line, id, binding, classname|
printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, classname if id.to_s == 'method_missing'
}

The result is like this:

ruby-1.8.7-p334 :036 > SomeModel.some_missing_method
call /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1873 method_missing ActiveRecord::Base
line /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1874 method_missing ActiveRecord::Base
line /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1874 method_missing ActiveRecord::Base
line /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1981 method_missing ActiveRecord::Base
line /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1998 method_missing ActiveRecord::Base
c-call /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1998 method_missing Kernel
raise /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1998 method_missing ActiveRecord::Base
c-return /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1998 method_missing Kernel
return /Users/.../.rvm/gems/ruby-1.8.7-p334/gems/activerecord-2.3.12/lib/active_record/base.rb:1998 method_missing ActiveRecord::Base

Ruby metaprogramming with method_missing to make a HTML DSL

Your problem is not that @result is reset, only that you add into the @result the return value of instance_eval(&block), which is the last line in the block, and not the aggregated block. This should work better (although not perfectly):

class HtmlDsl
attr_reader :result
def initialize(&block)
instance_eval(&block)
end

private

def method_missing(name, *args, &block)
tag = name.to_s
content = args.first
(@result ||= '') << "<#{tag}>"
if block_given?
instance_eval(&block)
else
@result << content
end
@result << "</#{tag}>"
end
end

So now:

html = HtmlDsl.new do
html do
head do
title 'yoyo'
end
body do
h1 'hey'
end
end
end
p html.result
#=> "<html><head><title>yoyo</title></head><body><h1>hey</h1></body></html>"

What I've done is that each call actually renders a fragment to the @result, so inner calls render inner fragments, each wrapping its own inner fragments with tags.



Related Topics



Leave a reply



Submit