During one of our bootcamp study sessions, we explored Ruby’s eval and discovered how powerful meta-programming in Ruby can be. It enables you to do almost anything at runtime, from evaluating complex expressions to creating classes, modules, methods, variables, and constants dynamically.
For starters, what is meta programming?
In simple terms, meta programming is about writing code that can generate more code.
Let me clarify, you write a code that generates more code during runtime which can then be executed dynamically.
It is not just about executing a logic like 2+2
or (a + b)²
Or calling a method dynamically like send(:my_method)
In ruby, you can write code that will create classes, modules and methods, variables dynamically. It can even extend existing classes/modules and do more.
Basic Eval
When I was new to ruby, the only thing I knew about eval was to use it for evaluating simple expressions. I thought that was it; I didn’t know it could do more.
You can use eval to execute arbitrary ruby code contained in a string like below:
eval("2+ 3+5")
While powerful, eval should be used cautiously as it executes code in the current binding and can pose security risks if used with untrusted input.
That was super simple but you can do much more. Let’s explore more.
Instance Evaluation
instance_eval allows you to evaluate code in the context of an object’s instance. This method is particularly useful when you need to:
- Access instance variables directly
- Define singleton methods on specific instances
- Modify a single object’s behaviour
Dynamically set or get instance variables
Let’s define a class with an instance variable
class Person
def initialize(name)
@name = name
end
end
person = Person.new("Magesh")
Use eval to dynamically set the instance variable like this:
eval "person.instance_variable_set(:@name, 'Dinesh')"
puts person.instance_variable_get(:@name) # Output: Dinesh
Use eval to dynamically get the instance variable like this:
eval_name = eval "person.instance_variable_get(:@name)"
puts eval_name # Output: Dinesh
Using class_eval
to create class
Let’s create a method that can create more classes by taking a name as an argument.
def create_dynamic_class(name, methods_to_add)
Object.class_eval <<-RUBY
class #{name}
attr_reader :created_at
def initialize(name)
@name = name
@created_at = Time.now
end
def greet
"Hello! Welcome"
end
#adding a class method
def self.description
"I am a dynamically created class named #{name}"
end
end
RUBY
end
Now whenever I want to create a class I can simply call the method like this:
create_dynamic_class('Dog', {})
# Let's execute the method and check
milo = Dog.new("Milo")
Dog.description # => I am a dynamically created class named Dog
That was simple, right? Now, how do we add instant methods to that class?
We have to tweak the code a little bit like adding a few more lines to the create_dynamic_class
method.
# Add instance methods using class_eval with a block
klass = Object.const_get(name)
# Add each method to the class
methods_to_add.each do |method_name, method_body|
klass.class_eval do
define_method(method_name, method_body)
end
end
The above code will take a hash with method names as keys and procs or blocks as values with the implementation.
We have to create a hash now to add some methods, let’s see how its done
This is the hash:
my_methods = {
greet: -> { "Ruf! Ruf!" },
say_bye: -> { "Woof! Woof!" },
birth_time: -> { "Born at: #{@created_at}" }
}
Now I have to call
create_dynamic_class("Dog", my_methods)
We can test it to check if all the methods are working:
milo = Dog.new("Milo")
milo.greet
milo.say_bye
milo.birth_time
that should give you the following output:
#<Dog:0x0000000110a16650 @created_at=2024-12-30 14:20:08.776167 -0500, @name="Milo">
"Ruf! Ruf!"
"Woof! Woof!"
"Born at: 2024-12-30 14:22:43 -0500"
See what we did? We have created a class dynamically and added a few instances and class methods to it. All during runtime. That was cool, right? Let’s try one more thing.
Define singleton methods on specific instances
What if we only want to add methods to a specific instance of a class and not to all the instances(objects). In our code above we created a dog named Milo but what if there is another dog and it could do different things that Milo didn’t?
simba = Dog.new("Simba")
Now, what if Simba could roll which Milo couldn’t?
simba.instance_eval do
def roll
"#{@name} is rolling over!"
end
end
Now, the above method roll
will only be available inside the instance simba.
simba.roll #=> "Simba is rolling over!"
milo.roll #=> undefined method `roll' for an instance of Dog (NoMethodError)
You see, if I call “roll” method on the instance “milo”. I would get an undefined method error because we added the roll method only on the instance “simba”. That’s how you create a singleton method for an instance.
Great. That’s enough for now. I’ll show you more in Part 2.
Source link
lol