Skip to Content
Course content

89: instance_eval and class_eval

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a project recently where I wanted to create a configuration DSL—you know, one of those clean blocks of code where you just list settings without having to repeat the object name over and over. I started with a simple class, but I quickly hit a wall regarding how Ruby handles scope.

The "Self" Problem

Let's look at what I first tried. I wanted a ProjectConfig class that could take a block of settings. It felt intuitive: I'd pass a block, and the class would run it.

class ProjectConfig
  attr_accessor :name, :version

  def initialize(&block)
    block.call if block
  end
end

config = ProjectConfig.new do
  self.name = "MyAwesomeApp"
  self.version = "1.0.0"
end

Now, this actually works, but it's clunky. I still have to use self.name because inside that block, self is actually the main object (the top-level scope), not the ProjectConfig instance. If I tried to just call name = "MyAwesomeApp", Ruby would think I'm trying to create a local variable called name, and my object would stay empty. That's not the "clean" feel I'm after.

Shifting Context with instance_eval

I wondered if there was a way to actually *move* the execution of that block inside the object. That's where I stumbled back into instance_eval. Let's try swapping block.call for instance_eval and see what happens to the scope.

class ProjectConfig
  attr_accessor :name, :version

  def initialize(&block)
    instance_eval(&block) if block
  end
end

config = ProjectConfig.new do
  # No 'self' needed anymore!
  @name = "MyAwesomeApp" 
  @version = "1.0.0"
end

puts config.name # => MyAwesomeApp

Wait, look at what happened. By using instance_eval, I've effectively told Ruby: "Take this block and execute it as if it were written right inside the instance of this class." Suddenly, self inside the block is the config object. I can access instance variables directly or call private methods that would normally be off-limits from the outside.

It's powerful, but a bit dangerous. If I have a local variable in my main script with the same name as a method in my class, instance_eval shifts the priority. It's essentially a "context switch."

Modifying the Blueprint with class_eval

While I was playing with this, I realized I wanted to do something different. I didn't want to change a specific *instance*; I wanted to add a new method to the ProjectConfig class itself, but I wanted to do it from outside the original class definition—maybe based on some plugin or external configuration.

I tried instance_eval on the class itself, like this:

ProjectConfig.instance_eval do
  def summary
    "#{name} v#{version}"
  end
end

When I ran this, I got a NoMethodError when calling config.summary. Why? Because ProjectConfig is an instance of the class Class. Using instance_eval on it added a class method, not an instance method. I could call ProjectConfig.summary, but not config.summary.

To fix this, I need class_eval. This tells Ruby: "Treat this block as if it were written inside the class ProjectConfig ... end block."

ProjectConfig.class_eval do
  def summary
    "#{name} v#{version}"
  end
end

puts config.summary # => MyAwesomeApp v1.0.0

Now the method is available to every single instance of ProjectConfig. The distinction is subtle but critical: instance_eval changes the context to a specific object (which could be a class object, resulting in a class method), while class_eval specifically targets the class's body to define instance methods.




📋 Practical Task

Building a Dynamic API Permission Guard

You are building a security module where permissions are defined dynamically. Your goal is to create a PermissionGuard class that uses instance_eval to allow a "clean" DSL for defining which roles have access to which actions.

Requirements:

  • Create a PermissionGuard class with an initialize method that accepts a block and uses instance_eval to execute it.
  • Inside the class, create a method called allow(role, action) that stores these permissions in a hash (e.g., @permissions = { "admin" => ["delete", "edit"] }).
  • Implement a method permitted?(role, action) that returns true or false.
  • Instantiate the guard using a block like this:
    guard = PermissionGuard.new do
          allow "admin", "delete"
          allow "editor", "edit"
        end
  • Verify that guard.permitted?("admin", "delete") returns true and guard.permitted?("editor", "delete") returns false.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.