Search code examples
rubyreflectionmoduleincludeextend

How to define a class-level macro in a module in Ruby?


In the Ruby programming language, I am creating a class with a class-level macro, as follows:

class Timer 
  def self.add_time
    def time
      STDERR.puts Time.now.strftime("%H:%M:%S")      
    end 
  end 
end 

The class method add_time, when executed, will generate a time method. Now, I can execute that class-level macro in another class Example as follows:

class Example < Timer 
  add_time 
end 

When I now call time on an instance of class Example, the time method is present there, as I intended:

ex = Example.new 
ex.time

and prints the current time: 23:18:38.

But now I would like to put the add_time macro in a module and still have the same overall effect. I tried with an include like this:

module Timer 
  def self.add_time
    def time
      STDERR.puts Time.now.strftime("%H:%M:%S")      
    end 
  end 
end 

class Example 
  include Timer
  add_time
end 

ex = Example.new 
ex.time

but then I receive an error that the method add_time is not defined on the class Example: NameError: undefined local variable or method ‘add_time’ for Example:Class. So then I tried with an extend instead like this:

class Example 
  extend Timer
  add_time
end

but it gives me a similar error.

So the question is: How can I get the same effect as in my original example where the Timer was defined as a class, but using a module instead?


Solution

  • As @CarySwoveland pointed out, the method def self.add_time in the module Timer gets disregarded upon inclusion or extension in a class. Only the module's instance methods are added to the class as instance method of the class (in case of inclusion) or as class methods of the class (in case of extends).

    module Timer 
      def add_time # INSTANCE METHOD !
        def time
          STDERR.puts Time.now.strftime("%H:%M:%S")      
        end 
      end 
    end 
    

    So the first step of the solution is to declare the method def add_time as an instance method of the module. Next, we extend the class Example with that module, so that the module's instance method gets added as a class method in the class Example, and we call the add_timemethod:

    class Example 
      extend Timer # EXTEND INSTEAD OF INCLUDE
      add_time
    end
    

    However, this doesn't quite work as desired yet as the time method has now been generated as a class method: Example.time prints the current time 01:30:37, but an instance ex of class Example does not understand the method time.

    The solution is thus to generate the method def time as an instance method rather than as a class method. This can be done using class_eval, which leads us to the following working solution:

    module Timer 
      def add_time # INSTANCE METHOD !
        self.class_eval do # USE class_eval TO DEFINE AN INSTANCE METHOD !
          def time
            STDERR.puts Time.now.strftime("%H:%M:%S")      
          end 
        end 
      end
    end 
    
    class Example 
      extend Timer # USE EXTEND TO ADD add_time AS A CLASS METHOD
      add_time
    end 
    
    ex = Example.new 
    ex.time