DEV Community

Cover image for Meta programming with Ruby Eval: A guide (Part 2)
Magesh for Railsfactory

Posted on • Originally published at railsfactory.com

Meta programming with Ruby Eval: A guide (Part 2)

Hope you read Part 1 of meta-programming with Ruby eval before coming here, if not you can click this link and read it first. It's kind of like a pre-requisite.

Meta programming with Ruby Eval - Part 1

In part one, we saw the basics of eval and how to dynamically create a class and add methods to it. Now let's see how we can extend a class, and also learn how to create modules and use them in classes, during runtime. Ready? Let's dive right in.

Extending a class

Let's say we already have a class file, we can extend it dynamically during runtime to add more instance and class methods.

Consider the below example Animal class:

class Animal
  def initialize(name)
    @name = name
  end
end
Enter fullscreen mode Exit fullscreen mode

To add more methods to it, we can simply use the following:

## Adding methods using class_eval
Animal.class_eval do
  def speak
    "#{@name} makes a sound"
  end

  def self.species
    @species ||= []
  end

  # Add attribute readers dynamically
  attr_reader :name
end
Enter fullscreen mode Exit fullscreen mode

The above code adds an instance method called "speak", a class method "species", and also an attribute reader to read the @name value.

Overriding methods in class or redefining existing behaviour

Now, let's assume the Animal class had a method called "name", like so:

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

  def name
    print "The name is #{@name}"
  end
end

Enter fullscreen mode Exit fullscreen mode

I can override it on the fly using the following code:

Animal.class_eval do
  # Store the original method
  alias_method :original_name, :name

  def name
    puts "You can call me #{@name}!"
  end
end
Enter fullscreen mode Exit fullscreen mode

I can use the class_eval to override any method in any class during runtime. But we have to be careful not to change an expected behaviour which might lead to misunderstanding and cause trouble for other developers.

Dynamically create Attribute methods

You want to write attribute methods: reader and writer, here's how

['age', 'color', 'breed'].each do |attribute|
  Animal.class_eval do
    # Create getter
    define_method(attribute) do
      instance_variable_get("@#{attribute}")
    end

    # Create setter
    define_method("#{attribute}=") do |value|
      instance_variable_set("@#{attribute}", value)
    end
  end
end

# Usage examples
dog = Animal.new("Rex")
puts dog.speak              # => "Loudly: Rex makes a sound"

dog.age = 5
dog.color = "brown"
puts "#{dog.name} is #{dog.age} years old and #{dog.color}"
Enter fullscreen mode Exit fullscreen mode

Create Modules using class_eval

In part one we saw how to create classes, similarly, can I create modules? and maybe include them in classes? Yes, we can. The following code does that:

# Creating a module 
module_code = <<-RUBY
  module Swimmable
    def swim
      "\#{@name} is swimming!"
    end
  end
RUBY
Enter fullscreen mode Exit fullscreen mode

Now that's the module code but I need to evaluate it first and then include it in a class to make it work, right? So how can we do that?

# Evaluate the module code and include it
Object.class_eval(module_code)
Animal.class_eval { include Swimmable }
Enter fullscreen mode Exit fullscreen mode

In the above code, I first evaluate the module code using Object.class_eval and then to include it in a class we used class_eval on the class itself. That's it. Now I can check it by using the following code:

animal = Animal.new("panda")
puts animal.swim
Enter fullscreen mode Exit fullscreen mode

That should work. Now you know how to create modules on the fly and include them in any existing class or we can also create a class on the fly and include the module in it.

Adding methods to a module

Let's create a real-world use case. A validation DSL:

module Validators
  def self.create_validator(field, &validation_logic)
    module_eval do
      define_method("validate_#{field}") do |value|
        if validation_logic
          instance_exec(value, &validation_logic)
        else
          value.nil? ? false : true
        end
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

You can use the Validators module to create basic presence validation like the following:

class User
  include Validators

  attr_accessor :email, :age, :username

  # Create basic presence validators
  create_validator :email
  create_validator :age
  create_validator :username
end

user = User.new
puts user.validate_email(nil)      # => false
puts user.validate_email("test@example.com")  # => true
Enter fullscreen mode Exit fullscreen mode

If you want to create a custom validation to check the price value, etc. You can do something like this:

create_validator(:price) { |value| value.to_f > 0 }
Enter fullscreen mode Exit fullscreen mode

See how helpful this can be?

Extending Modules

module Extensions
  module_eval do
    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      def class_method
        puts "This is a class method"
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Class and Module Evaluation

module_eval (also aliased as module_exec) is a method that allows you to evaluate code in the context of a module. Like class_eval, it lets you define or modify methods that will be instance methods of classes that include the module.

module Greeting
  def self.add_greeting(name)
    module_eval(<<-RUBY)
      def greet_#{name}
        puts "Hello, #{name}!"
      end
    RUBY
  end
end

class Person
  include Greeting
end

Greeting.add_greeting("alice")
person = Person.new
person.greet_alice  # Output: Hello, alice!
Enter fullscreen mode Exit fullscreen mode

While module_eval and class_eval are very similar, there are some key differences:

Context: module_eval is specifically for modules, while class_eval is for classes. However, since classes are also modules in Ruby (Class inherits from Module), you can use module_eval on classes too.

# These are equivalent for classes
MyClass.class_eval do
  def some_method
    puts "Hello"
  end
end

MyClass.module_eval do
  def some_method
    puts "Hello"
  end
end
Enter fullscreen mode Exit fullscreen mode

module_eval is often used in module methods to define methods dynamically that will be available to all classes including that module.

Dynamic Method Definition

define_method is a powerful way to create methods dynamically:

class APIWrapper
  ['get', 'post', 'put', 'delete'].each do |http_method|
    define_method(http_method) do |url, params = {}|
      # Generic HTTP request handling
      puts "Making #{http_method.upcase} request to #{url}"
    end
  end
end

api = APIWrapper.new
api.get('/users')   # => "Making GET request to /users"
api.post('/users')  # => "Making POST request to /users"
Enter fullscreen mode Exit fullscreen mode

Removing Methods

Just as we can define methods dynamically, we can remove them:

class Example
  def temporary_method
    "I won't be here long"
  end

  remove_method :temporary_method
end
Enter fullscreen mode Exit fullscreen mode

Dynamic Constant and Variable Management

Setting Constants

module Configuration
  const_set(:API_VERSION, "v1")
  const_set(:MAX_RETRIES, 3)
end

puts Configuration::API_VERSION  # => "v1"
Enter fullscreen mode Exit fullscreen mode

Variable Operations

class StateManager
  class_variable_set(:@@state, {})

  def self.state
    class_variable_get(:@@state)
  end

  def update_state(key, value)
    instance_variable_set("@#{key}", value)
  end
end
Enter fullscreen mode Exit fullscreen mode

Best Practices and Warnings

Security Considerations

  • Never use eval with untrusted input
  • Prefer more specific evaluation methods over basic eval
  • Use define_method instead of eval when defining methods dynamically

Performance Impact

  • Evaluation methods are slower than static definitions
  • Cache results when doing repeated evaluations
  • Consider using metaprogramming during class loading rather than runtime

Code Readability

  • Document why you're using metaprogramming
  • Keep dynamic code generation simple and obvious
  • Consider whether a more straightforward approach might work better

Ruby's evaluation methods provide powerful tools for metaprogramming, allowing you to write more dynamic and flexible code. While these tools should be used judiciously, understanding them opens up new possibilities for solving complex problems elegantly.

Remember that with great power comes great responsibility – always consider whether metaprogramming is the best solution for your specific use case, and document your code well when you do use it.

That's all for now. Thank you for reading it till the end.

Top comments (0)