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.