DEV Community

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

Posted on • Originally published at railsfactory.com

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

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Use eval to dynamically get the instance variable like this:

eval_name = eval "person.instance_variable_get(:@name)"
puts eval_name # Output: Dinesh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}" }
}
Enter fullscreen mode Exit fullscreen mode

Now I have to call

create_dynamic_class("Dog", my_methods)
Enter fullscreen mode Exit fullscreen mode

We can test it to check if all the methods are working:

milo = Dog.new("Milo")
milo.greet
milo.say_bye
milo.birth_time
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Now, what if Simba could roll which Milo couldn't?

simba.instance_eval do
  def roll
    "#{@name} is rolling over!"
  end
end
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Go to part 2 to read more

Top comments (0)