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
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
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
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
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}"
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
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 }
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
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
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
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 }
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
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!
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
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"
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
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"
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
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)