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)