0

I'm attempting to create a module that sets and instance variable called @evaluator on it's included class. I then want to access that variable in method_missing of the included class:

module ObjectInquiry
  def self.included(base)
    base.class_eval do

      def method_missing(method, *args, &block)
        @evaluator.call
      end
      def self.inquiry(method_name = nil, &block)
        @evaluator = block if block_given?
      end
    end
  end
end

class Post
  include ObjectInquiry

  inquiry do
    true
  end
end

If I then do:

Post.new.awesome?

I get NoMethodError: undefined method 'call' for nil:NilClass. I'm guessing this is because I'm improperly setting evaluator or something. What is the proper way to do this?

2
  • 1
    The @evaluator you set is on class level, the @evaluator you try to read is on instance level. The class and the instance are two different objects and therefore the two @evaluator are different variables. Commented Dec 22, 2014 at 6:11
  • 1
    When you call the class method inquiry you set the class instance variable @evaluator. When you call method_missing you send call to the uniititalized instance variable with the same name. The two variables are as different as @dog and @cat. Commented Dec 22, 2014 at 6:12

1 Answer 1

2

First of all your module structure is a bit unidiomatic. To define instance methods, we can simply put a def inside the module. To define class methods, one would typically define a nested module ClassMethods and call base.extend in the include hook. It saves a level of indentation - this doesn't seem like much but it can add up to quite awkward code. The following modules are equivalent:

module A
  def self.included(base)
    base.class_eval do
      def instance_method
        puts 'Hello from instance A!'
      end

      def self.class_method
        puts 'Hello from class A!'
      end

      foo() # code evaluated in class body
    end
  end
end

module B
  def self.included(base)
    base.class_eval do
      foo()
    end

    base.extend ClassMethods
  end

  def instance_method
    puts 'Hello from instance B!'
  end

  module ClassMethods
    def class_method
      puts 'Hello from class B!'
    end
  end
end

If you're using Rails or require 'active_support/concern' you can even extend ActiveSupport::Concern to reduce the amount of boilerplate code:

module C
  extend ActiveSupport::Concern

  included do
    foo()
  end

  def instance_method
    puts 'Hello from instance B!'
  end

  module ClassMethods
    def class_method
      puts 'Hello from class B!'
    end
  end
end

The next thing you need to understand is the difference between instance variables and class instance variables. For example this is how to set an instance variable and then retrieve it in context of the instance

class C
  def set_value
    @x = 123
  end

  def get_value
    @x
  end
end

And this is how to set a class instance variable and then retrieve it in context of the instance. If you think of class D as instance of the class Class, then the term "class instance variable" makes kind of sense. I think the simplest way to access a class instance variable from an instance of the class is to define an attr_accessor in the class' Eigenclass.

class D
  class << self
    attr_accessor :x
  end

  def self.set_value
    self.x = 123          # self refers to the class, we need to use self.x
  end

  def get_value
    self.class.x          # self refers to the instance, we need to use self.class.x
  end
end

All of these hints result in the following code:

module ObjectInquiry
  def self.included(base)
    base.class_eval do
      class << self
        attr_accessor :evaluator
      end
    end

    base.extend ClassMethods
  end

  def method_missing(method, *args, &block)
    self.class.evaluator.call
  end

  module ClassMethods
    def inquiry(method_name = nil, &block)
      self.evaluator = block if block_given?
    end
  end
end

Testing:

class Post
  include ObjectInquiry

  inquiry do
    true
  end
end

Post.new.flabbergasted?
#=> true
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.