Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compose modules containing method_missing in ruby

I have a couple of modules that extend method missing:

module SaysHello
    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^hello/)
    end
    def method_missing(method, *args, &block)
        if (method.to_s =~ /^hello/)
            puts "Hello, #{method}"
        else
            super.method_missing(method, *args, &block)
        end
    end
end

module SaysGoodbye
    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^goodbye/)
    end
    def method_missing(method, *args, &block)
        if (method.to_s =~ /^goodbye/)
            puts "Goodbye, #{method}"
        else
            super.method_missing(method, *args, &block)
        end
    end
end

class ObjectA
    include SaysHello
end

class ObjectB
    include SaysGoodbye
end

This all works well, eg ObjectA.new.hello_there outputs "Hello, hello_there". Likewise, ObjectB.new.goodbye_xxx outputs "Goodbye, xxx". respond_to? also works, eg ObjectA.new.respond_to? :hello_there return true.

However, this doesn't work very well when you want to use both SaysHello and SaysGoodbye:

class ObjectC
    include SaysHello
    include SaysGoodbye
end

While ObjectC.new.goodbye_aaa works correctly, ObjectC.new.hello_a acts strange:

> ObjectC.new.hello_aaa
Hello, hello_aaa
NoMethodError: private method `method_missing' called for nil:NilClass
    from test.rb:22:in `method_missing' (line 22 was the super.method_missing line in the SaysGoodbye module)

It outputs correctly, then throws an error. Also respond_to? doesn't correctly, ObjectC.new.respond_to? :hello_a returns false.

Finally, adding this class:

class ObjectD
    include SaysHello
    include SaysGoodbye

    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^lol/)
    end


    def method_missing(method, *args, &block)
        if (method.to_s =~ /^lol/)
            puts "Haha, #{method}"
        else
            super.method_missing(method, *args, &block)
        end
    end
end

Also acts strangely. ObjectD.new.lol_zzz works, however ObjectD.new.hello_aand ObjectD.new.goodbye_t both throw a name exception after outputting the correct string. respond_to? also fails for hello and goodbye methods.

Is there a way to get this all working correctly? An explanation of how method_missing, Modules and super are interacting would also be really useful.

EDIT: coreyward solved the problem, if I use super instead of super.<method-name>(args...) in all the methods I define, the program works correctly. I don't understand why this is though, so I asked another question about this at What does super.<method-name> do in ruby?

like image 238
David Miani Avatar asked Jun 16 '11 02:06

David Miani


People also ask

What is Method_missing in Ruby?

method_missing is a method that ruby gives you access inside of your objects a way to handle situations when you call a method that doesn't exist. It's sort of like a Begin/Rescue, but for method calls. It gives you one last chance to deal with that method call before an exception is raised.

How do you create a module in Ruby?

Creating Modules in Ruby To define a module, use the module keyword, give it a name and then finish with an end . The module name follows the same rules as class names. The name is a constant and should start with a capital letter. If the module is two words it should be camel case (e.g MyModule).

How do you reference a module in Ruby?

As with class methods, you call a module method by preceding its name with the module's name and a period, and you reference a constant using the module name and two colons.

How do you access a module method in Ruby?

The module methods & instance methods can be accessed by both extend & include keyword. Regardless of which keyword you use extend or include : the only way to access the module methods is by Module_name::method_name.


2 Answers

When you redefine a method, you redefine a method; period.

What you're doing when you include the second module with the method_missing method define is overriding the previously defined method_missing. You can keep it around by aliasing it before you redefine it, but you might want to watch out with that.

Also, I don't know why you're calling super.method_missing. Once your method_missing definition is out of tricks you should let Ruby know it can continue on up the chain looking for a way to handle the call, all just by calling super (no need to pass arguments or specify a method name).

About Super (update)

When you call super Ruby continues on up the inheritance chain looking for the next definition of the method invoked, and if it finds one it calls it and returns the response. When you call super.method_missing you call the method_missing method on the response to super().

Take this (somewhat silly) example:

class Sauce
  def flavor
    "Teriyaki"
  end
end

# yes, noodles inherit from sauce:
#   warmth, texture, flavor, and more! ;)
class Noodle < Sauce
  def flavor
    sauce_flavor = super
    "Noodles with #{sauce_flavor} sauce"
  end
end

dinner = Noodle.new
puts dinner.flavor     #=> "Noodles with Teriyaki sauce"

You can see that super is a method just like any other, it just does some magic behind the scenes. If you call super.class here you're going to see String, since "Teriyaki" is a string.

Make sense now?

like image 199
coreyward Avatar answered Oct 06 '22 18:10

coreyward


http://www.perfectline.ee/blog/activerecord-method-missing-with-multiple-inheritance

This article explains exactly how it works: Each new module doesn't overwrite or replace methods - instead, its methods are inserted into the inheritance chain. That's why calling super from each method_missing eventually calls all the method_missing's.

The class remains lowest in the inheritance chain, and the last-added module is adjacent to the class.

So:

class Foo
  include A
  include B
end

results in Kernel -> A -> B -> Foo

like image 29
ChrisPhoenix Avatar answered Oct 06 '22 18:10

ChrisPhoenix