How to compose modules containing method_missing in ruby
Asked Answered
P

2

9

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?

Popeyed answered 16/6, 2011 at 2:28 Comment(1)
Included modules are added to the inheritance chain; they don't override or replace methods. So if each method_missing calls super, eventually they'll all be called. See my answer below.Higgs
S
6

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?

Som answered 16/6, 2011 at 2:33 Comment(4)
But importing the second module didn't override the first module's method_missing, otherwise ObjectC.new.hello_a would not have outputted Hello, hello_a (plus a NameError). Using super instead of super.method_missing(...) actually solved the problem, all the method_missing calls and respond_to? calls now work correctly. I don't understand why though.Popeyed
@nanothief: I've updated the question to (hopefully) explain more about how super works in Ruby. I think it'll clear things up for you.Som
thanks, that was the problem I was having with super. I was assuming super.method was calling method on the parent class, not calling method on the result of calling the current method on the superclass.Popeyed
Perhaps should clarify that "override" means "insert into the inheritance chain" and "super" means "call higher up the inheritance chain". That's why calling super from each method_missing eventually calls all the method_missing's.Higgs
H
2

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

Higgs answered 10/12, 2013 at 17:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.