The difference between Ruby’s instance_eval and class_eval is bamboozling enough that I catch myself off guard sometimes using one when I meant to use the other. This post aims to distil some of the dissimilarities and use cases of the two.

Let’s get one difference between instance_eval and class_eval straight before we proceed:

You can call:

  • instance_eval on both arbitrary objects (instances of classes) and on classes.
  • class_eval only on classes (instances of the Class class).

Understanding instance_eval

instance_eval comes from BasicObject and it has two signatures. Let’s talk first about the signature you’re likely to see more often:

instance_eval {|obj| block } -> obj

instance_eval on an arbitrary object

With this, you pass a block to instance_eval and get an object back. We’re going to first call this method on both arbitrary objects then on the classes of these objects. Our mock class is going to look like this:

class Klass
def initialize(name)
@name = name
end

private

def classified
puts "private stuff"
end
end

Let’s instantiate this class:

kl = Klass.new("Klein")

From this snippet, we have a Klass class and a kl object.

Here’s something trippy though. The Klass class itself is an object from Ruby’s Class class, which means, technically both Klass and kl are objects, but let’s not dive into that realm, we’ll leave it for another day.

We want to deal with just our Klass class and our kl instantiated objects today.

We have our class and object. The kl object is #<Klass:0x00007fd0f8d190b8 @name="Klein">, we see its class and instance variable, but there’s no way to access the instance variable directly because we haven’t defined a getter, in some cases we can’t or don’t want to, but at some point, we’d like to grab the @name instance variable.

If we tried to access the @name variable like so:

kl.name

We’d be greeted with:

undefined method `name' for
#<Klass:0x00007fdd7232e590 @name="Klein"> (NoMethodError)

We can force our way to reach the @name variable from the kl object with the following:

kl.instance_eval { @name }
#=> "Klein"

This is not what you’d want to be doing regularly though: writing out instance_eval { @name } just to get the @name variable is tedious. We can define a convenient method on the kl object that’ll give just the value of @name.

kl.instance_eval do
def name = @name
end

From this moment we can do kl.name to get "Klein" anytime we need to. Methods defined this way are only accessible by the objects instance_eval is called on.

For our second object, if we tried km.name, we’d get:

undefined method 'name' for
#<Klass:0x00007fc8e5069090 @name="Calvin"> (NoMethodError).

You can deal with Ruby’s method_missing in a few ways, but that’s out of scope now.

This demonstrates that instance_eval evaluates a block of code in the context of the receiver.

instance_eval Can Access Private Methods

One thing to note about intance_eval is, not only can it access instance variables of the receiver, it can also access private methods. So if you take a second peek at our mock class, you’ll notice we defined some private method in there, we can reach that private method with instance_eval with no problem.

kl.instance_eval { classified }

# => "private stuff"

The reason this is possible is that when a block is passed to instance_eval, it is evaluated with the receiver as self. To set the context, the variable self is set to kl while the code is executing. This gives the evaluated code access to kl’s instance variables and private methods.

Everything discussed in the last few paragraphs is evident in this snippet:

class Klass
def initialize(name)
@name = name
end

private def greet = "hi"
end

kl = Klass.new("Klein")

kl.instance_eval do

# `self` has been set to `kl`

self == kl # => true

# And since `self` is now `kl`

# we can access instance variables

# and private methods

@name # => "Klein"
greet # => "hi"
end

However, with everything we’ve learned so far, instead of using instance_eval to access private methods, I believe doing kl.send(:classified) clarifies the intent better. This, of course, should depend on your use case.

Calling instance_eval With A String

Everything we’ve done up until this point dealt with passing a block to instance_eval. The second signature of instance_eval concerns the case where you pass a string to instance_eval:

instance_eval(string [, filename [, lineno]] ) -> obj

Usage of instance_eval this way is almost always used when you want to properly report compilation errors. In this case, the filename and lineno options point you to where errors occur at runtime so you can track and fix them. If you’re tracking where errors occur, the last two options will most likely be a reference to the current file and line numbers being executed respectively.

Let’s see how it’s used.

First, we’ll modify our mock class to look like this:

class Klass
def initialize(name)
@name = name
end
end

kl = Klass.new("Klein")

kl.instance_eval <<-CODE, **FILE**, **LINE**
def compute_age(current_year, year_of_birth)
puts current_year - year_of_birth
end
CODE

kl.compute_age("2022", 1940)

This shows how you’d typically use intances_eval when you pass it a string. Here, we’re passing "2022" as current_year and subtracting 1940. Just like you might have expected, we’d get an error, we’re trying to subtract an integer from a string, which makes no sense. With this we’ll get:

rb.rb:10:in `compute_age': undefined method `-' for
"2022":String (NoMethodError)
Did you mean? -@
from rb.rb:15:in `<main>'

Note how this nicely reports the name of the file and the line number that executes the code. The name of the file is rb.rb, and the line number is 10. We can go navigate to this point and fix our errors.

If we were to pass just a string, it’d mean we’re sure that the code we want to be evaluated is error-free, and since the file name and line numbers are optional, the string to intance_eval will be evaluated anyway, but we’ll have a hard time if we were working in a complex codebase without a way to report where the error is coming from.

Here’s the result of the same code without __FILE__ and __LINE__:

(eval):2:in `compute_age': undefined method `-' for
"2022":String (NoMethodError)
Did you mean? -@
from rb.rb:15:in `<main>'

For the file name, we have eval and for the line number in the report, we have 2, which is not true.

instance_eval On An Arbitrary Class

When you call instance_eval, to iterate, you’re evaluating some code in the context of the receiver, so when we called instance_eval on kl we were executing code in the context of kl, in our example, we defined some method, that method could only be used on kl and not on other instances of Klass.

When we use instance_eval on a class, we’re executing code in the context of that specific class–that class is now set to self, so if we defined a method, we’d be writing class methods for the class we called instance_eval on.

Say we have this now:

class Klass
def initialize(name)
@name = name
end
end

Klass.instance_eval do
def identity
puts object_id
end
end

We can now call Klass.identity to get the object ID of this class. We’ve defined a method in the context of Klass, which is not a class method.

Demystifying class_eval

class_eval has an identical signature to instance_eval and the usage is similar, but instance_eval is defined in BasicObject making it possible to call it on anything.

Conversely, class_eval is defined in Module and can only be called on classes or instances of Moduleclass_eval also evaluates a string or block in the context of a receiving Class instance (keep in mind that a Class is a special kind of a Module). In fact class_eval is an alias to module_eval. Checks out.

When we call class_eval, we’re evaluating a block of code or string in the context of the class. This allows us to reopen a class and do whatever you want in it. When we call instance_eval we’re evaluating a block of code or string in the context of the receiver too, this time though, the object could be anything because instance_eval is defined at the very top, inside BasicObject.

With that out of the way. We can write code like:

class Klass
def initialize(name)
@name = name
end
end

Klass.class_eval do
def greeting
puts "hello"
end
end

And expect that any object instantiated from Klass will now be able to access the method greeting:

ko = Klass.new("Klein")
mo = Klass.new("Klein")

ko.greeting # => "hello"
mo.greeting # => "hello"

Conclusion

Remember that instance_eval is defined on BasicObject and for that matter, every object can access it, when called on an object, self is set to that object and any block passed to instance_eval is ran in the context of the object on which it was invoked.

class_eval does a similar thing, in the sense that it runs code in the context of the receiver, but because class_eval is an instance method inside Module it can only be called on instances of Module or the Class class which is itself a special kind of Module.

Last Update: January 07, 2024