0

I've written a little wrapper over method_missing like this:

module Util
  def method_missing_for(regex, &blk1)
    define_method(:method_missing) do |name, *args, &blk2|
      match = name.to_s.scan(regex).flatten[0]
      if match
        blk1.call(match, *args, &blk2)
      else
        super(name, *args, &blk2)
      end
    end
  end
end

Then using it:

class Foo
  extend Util
  method_missing_for(/^say_(.+)$/){ |word| puts word }
end

Foo.new.say_hello
# => "hello"

The problem is that I cannot call this multiple times for a class. The method_missing I add with define_method just gets overridden. What alternative do I have? Conceptually I know I can refactor method_missing_for to take multiple regex => block mappings and then call it once instead of multiple times. At its core it would be a big case statement which tests all the regex. But I would rather be able to take advantage of super.

class Foo
  extend Util
  method_missing_for(/^say_(.+)$/) { |word| puts word }
  method_missing_for(/foobar/) {}
end
Foo.new.say_hello # => NoMethodError
max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • 1
    Strategically you'll want to define a singular `method_missing` method that intercepts and dispatches through some kind of lookup table. `method_missing_for` should create that method if it doesn't exist, then append an entry to the mapping table. – tadman Mar 20 '17 at 22:38
  • thanks @tadman that makes sense, I was just posting this to see if there's some way around it I wasn't aware of. – max pleaner Mar 20 '17 at 22:43
  • That's how a lot of DSL-type meta-programming methods work. You need to have some kind of state you're manipulating in addition to your bridge method. – tadman Mar 20 '17 at 22:48
  • @tadman I've heard of the concept of adding method to an inheritance chain, so I was wondering if that could be a way – max pleaner Mar 20 '17 at 22:50
  • If I make an anonymous module could I append it to the ancestors list? – max pleaner Mar 20 '17 at 22:58
  • 1
    @maxple yes you can. – Aetherus Mar 20 '17 at 22:58

2 Answers2

0

My idea is to register the definition when declaring, and define those methods when all the declaration is done. The usage will be a little bit different, but I think is still acceptable

class Foo
  extend Util

  phantom_methods do
    method_missing_for(/^say_(.+)$/){ |word| puts word }
    method_missing_for(/^foo_(.+)$/){ |word| puts word }
  end
end

The implementation of Util will be

module Util
  def phantom_methods
    # Initialize the registration table
    @_phantom_methods = {}

    # Allow declaring methods
    yield

    # Consume the registration table and define a single `method_missing`
    define_method(:method_missing) do |name, *args, &blk|
      pair = self.class.instance_variable_get(:@_phantom_methods).find {|regexp, prc| name =~ regexp}
      pair ? pair[1].call($1) : super(name, *args, &blk)
    end

    # Good practice to also define `respond_to_missing?`
    define_method(:respond_to_missing?) do |name, include_private = false|
      self.class.instance_variable_get(:@_phantom_methods).any? {|regexp, _| name =~ regexp}
    end
  end

  # Declare a phantom method
  def method_missing_for(regexp, &blk)
    # Register the definition
    @_phantom_methods[regexp] = blk
  end
end

By the way, I borrowed the phrase phantom method from this book.

Aetherus
  • 8,720
  • 1
  • 22
  • 36
0

The problem is that I cannot call this multiple times for a class. The method_missing I add with define_method just gets overridden.

No, it doesn't. It gets overwritten.

You already have the solution in that sentence: you need to override it instead:

module Util
  def method_missing_for(regex, &blk1)
    prepend(Module.new do
    ##################### this is the only change compared to your code
      define_method(:method_missing) do |name, *args, &blk2|
        match = name.to_s.scan(regex).flatten[0]
        if match
          blk1.(match, *args, &blk2)
        else
          super(name, *args, &blk2)
        end
      end
    end)
    ####
  end
end

class Foo
  extend Util
  method_missing_for(/^say_(.+)$/) { |word| puts word }
  method_missing_for(/foobar/) {}
end
Foo.new.say_hello
# hello

Note: it may or may not be beneficial to name the module, so that it shows up with a sensible name in the ancestors chain, which improves the debugging experience:

Foo.ancestors
#=> [#<Module:0x007fa5fd800f98>, #<Module:0x007fa5fd801df8>, Foo, Object, Kernel, BasicObject]

See also When monkey patching a method, can you call the overridden method from the new implementation? for a more comprehensive treatment.

Community
  • 1
  • 1
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Thanks, I was currently working on a solution from your answer to [what-purpose-can-anonymous-modules-serve](http://stackoverflow.com/questions/32075993/what-purpose-can-anonymous-modules-serve) which pretty much looks like this – max pleaner Mar 21 '17 at 00:13
  • Funny that I added in the paragraph about naming :-D – Jörg W Mittag Mar 21 '17 at 00:17