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)