class_eval, instance_eval and instance_exec in Ruby

Ruby is a powerful and flexible programming language that allows developers to dynamically manipulate objects, classes, and methods. To accomplish this, Ruby provides three methods: instance_eval, instance_exec, and class_eval. These methods are often used for metaprogramming tasks, providing a way to modify or extend the behavior of objects and classes at runtime.
In this article, we will explore these methods by analyzing a sample code using the MyWebServer class.

Starting point

Before we dive into metaprogramming concepts, let's take a quick look at the MyWebServer class, which serves as the foundation for our examples.

class MyWebServer
  attr_reader :config

  def initialize
    @config = {
      domain: 'localhost',
      routes: {}
    }
  end

  def configure(&block)
    instance_eval(&block)
  end

  def domain(value)
    @config[:domain] = value
  end

  def route(path, &block)
    @config[:routes][path] = block
  end

  def start
    puts "Starting the web server on #{@config[:domain]}:#{@config[:port]}"
    puts "My routes:"
    @config[:routes].each do |path, _|
      puts "- #{path}"
    end
  end

  def self.version
    'Server version: 1.0.0'
  end

  private

  def say_private_hello(name = @config[:domain])
    puts "hello from private method of MyWebServer for #{name}"
  end
end

We have written a typical server configurator with some basic methods. The domain is passing the domain name to the @config variable. The route is getting the path of the route and then it can put the code from the block. Then the start shows us config and version is static and prints the version of the server. Now let's jump into the curious configure method.

The instance_eval usage

The instance_eval method is called on an object and evaluates a block of code within the context of that object. This means that any code within the block can directly access and modify the instance variables and methods of that object.

server = MyWebServer.new

# server.say_private_hello # that will raise an error because its a private method
server.instance_eval do |my_instance|
  puts my_instance.config
  # that will work because we're inside the instance_eval block
  say_private_hello
end

We'll set up the instance of MyWebServer and use instance_eval to show some data from config. You can see that inside the block we can use private methods as public. What if we want to make it cleaner to read? we can use the configure method from the class above!

# configure is a nicer method name than instance_eval
server.configure do
  domain 'mysite.com'

  route '/' do
    'Welcome to the homepage!'
  end
end

# but still we can use instance_eval directly
server.instance_eval do
  route '/about' do
    'This is the about page.'
  end
end

We're using a common DSL language configure that allows the domain and route to be passed in a block. You also see that the configure is equal to the instance_eval below.

The instance_exec usage

What is the difference between instance_eval and instance_exec? We can pass an argument to the block!. Let's look at an example:

# Using instance_exec allows to pass arguments to the block
my_custom_host_name = "instance_exec_site.com"
server.instance_exec(my_custom_host_name) do |new_site_host|
  domain new_site_host
  say_private_hello
end

As you can see, we can pass a custom variable my_custom_host_name within this block to set the domain.

The class_eval usage

The class_eval method, also known as module_eval, is called on a class or module and allows you to evaluate a block of code within the context of that class or module. This means that you can add or redefine methods, access class variables, and perform other class-level modifications.

# override the version method using class_eval
server.class.class_eval do
  def version
    # to get instance variables we need to use instance_variable_get
    puts "Overriding self.version method for config #{instance_variable_get(:@config)[:domain]}"
  end
end

server.version # => Overriding self.version method for config mysite.com
MyWebServer.new.version # => Overriding self.version method for config localhost

In this example, we'll override the version's method, which will print a different message.

Another typical usage of class_eval

Most of the time this method is used to dynamically create some methods, setters, getters, etc. Here is an example:

class AppConfig
  def self.configure(&block)
    class_eval(&block)
  end

  def self.default_configuration
    @configuration ||= {}
  end

  def self.setting(name, default_value)
    default_configuration[name] = default_value

    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{name}
        config = AppConfig.default_configuration
        config[:#{name}] || #{default_value.inspect}
      end

      def #{name}=(value)
        AppConfig.default_configuration[:#{name}] = value
      end
    RUBY
  end
end

# Let's use the configuration to set some default values.
AppConfig.configure do
  setting :app_name, "My Web App"
  setting :api_key, nil
end

# use class_eval than nice configure block
AppConfig.class_eval do
  setting :max_users, 1000
end

config = AppConfig.new
puts config.app_name    # => "My Web App"
puts config.api_key     # => nil
puts config.max_users   # => 1000

config.app_name = "My New App"
puts config.app_name    # Output: "My New App"

The AppConfig class provides an easy way to configure an object with default settings. It does this by using the class_eval method to dynamically define methods for each setting. The settings are stored in a hash called @configuration, which is initialized as an empty hash.

The configure method

class AppConfig
  def self.configure(&block)
    class_eval(&block)
  end
  # ...
end

The first important method in the AppConfig class is configure, which takes a block as an argument. This method calls the block using the class_eval method, which effectively makes the block's code run in the context of the AppConfig class. This way, any method calls within the block are executed as if they were part of the class definition.

The setting method

class AppConfig
  # ...
  def self.setting(name, default_value)
    default_configuration[name] = default_value

    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{name}
        config = AppConfig.default_configuration
        config[:#{name}] || #{default_value.inspect}
      end

      def #{name}=(value)
        AppConfig.default_configuration[:#{name}] = value
      end
    RUBY
  end
end

The next essential method is setting, which is responsible for defining individual settings. It takes two arguments: name - the name of the setting and default_value - the default value of the setting. It stores the name and default_value in the default_configuration hash.
Then, again using class_eval, it dynamically defines two methods for each setting: a reader method and a writer method. The reader method returns the value of the setting from the default_configuration hash, or default_value if the setting is not explicitly set. The writer method updates the default_configuration hash with the new value of the preference setting.

Configuring AppConfig

To use the AppConfig class, you can call the configure method and pass a block with the desired settings. The class_eval method then takes care of defining the appropriate methods for each setting.

AppConfig.configure do
  setting :app_name, "My Web App"
  setting :api_key, nil
end

AppConfig.class_eval do
  setting :max_users, 1000
end

Using AppConfig

Now that we've configured our AppConfig class with some default settings, let's create an instance of AppConfig and see how it behaves.

config = AppConfig.new
puts config.app_name    # => "My Web App"
puts config.api_key     # => nil
puts config.max_users   # => 1000

As you can see, we can use the reader methods to access the default values we set earlier. If we need to change any of these values, we can use the writer methods.

config.app_name = "My New App"
puts config.app_name    # Output: "My New App"

Note that instance_eval and class_eval can have different behavior when it comes to the context of self and variable scope. When using instance_eval, self inside the block is the receiver object itself, so instance variables and methods are directly accessible. However, with class_eval, self inside the block refers to the class/module itself, so instance variables are not directly accessible unless you use instance_variable_get and instance_variable_set.

Keep in mind that while metaprogramming can be powerful, it can also lead to code that is difficult to maintain and understand. Use these methods judiciously and with care.

Happy coding!

Full gist here.