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 Module
. class_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
.