Search code examples
rubyclass-methodinstance-method

Why does instance_eval() define a class method when called on a class?


Foo = Class.new
Foo.instance_eval do
  def instance_bar
    "instance_bar"
  end
end
puts Foo.instance_bar       #=> "instance_bar"
puts Foo.new.instance_bar   #=> undefined method ‘instance_bar’

My understanding is that calling instance_eval on an object is supposed to allow you to define an instance variable or method for that object.

But in the example above, when you call it on class Foo to define the instance_bar method, instance_bar becomes a class method that can be invoked with "Foo.instance_bar". It is clear that this code has not created an instance method because Foo.new.instance_bar results in "undefined method ‘instance_bar’".

Why does instance_eval define a class method rather than an instance method in this context?


Solution

  • x.instance_eval changes your context so self evaluates to x.

    This allows you to do many things, including defining instance variables and instance methods but only for x.

     x = Object.new
     y = Object.new
    
     # define instance variables for x and y
     x.instance_eval { @var = 1 }
     y.instance_eval { @var = 2 }
    
     # define an instance method for all Objects
     class Object
       def var
         @var
       end
     end
    
     x.var #=> 1
     y.var #=> 2
    

    Ruby lets you define instance methods for an object in a couple places. Normally, one defines them in a class, and those instance methods are shared among all instances of that class (like def var above).

    However, we can also define an instance method for just a single object:

    # here's one way to do it
    def x.foo
      "foo!"
    end
    # here's another
    x.instance_eval do
      # remember, in here self is x, so bar is attached to x.
      def bar
        "bar!"
      end
    end
    

    Even though x and y have the same class, they don't share these methods, since they were only defined for x.

    x.foo #=> "foo!"
    x.bar #=> "bar!"
    y.foo #=> raises NoMethodError
    y.bar #=> raises NoMethodError
    

    Now in ruby, everything's an object, even classes. Class methods are just instance methods for that class object.

    # we have two ways of creating a class:
    class A 
    end
    # the former is just syntatic sugar for the latter
    B = Class.new
    
    # we have do ways of defining class methods:
    
    # the first two are the same as for any other object
    def A.baz
      "baz!"
    end
    A.instance_eval do
       def frog
         "frog!"
       end
    end
    
    # the others are in the class context, which is slightly different
    class A
      def self.marco
        "polo!"
      end
      # since A == self in here, this is the same as the last one.
      def A.red_light
        "green light!"
      end
    
      # unlike instance_eval, class context is special in that methods that
      # aren't attached to a specific object are taken as instance methods for instances
      # of the class
      def example
         "I'm an instance of A, not A itself"
      end
    end
    # class_eval opens up the class context in the same way
    A.class_eval do
      def self.telegram
        "not a land shark"
      end
    end
    

    Note again, that all these methods are A-specific, B doesn't get access to any of them:

    A.baz #=> "baz!"
    B.telegram #=> raises NoMethodError
    

    The important thing to take away from here is that class methods are just instance methods of an object of class Class