loading...
Cover image for Changing Ruby classes at runtime with class_eval

Changing Ruby classes at runtime with class_eval

vinistock profile image Vinicius Stock ・4 min read

One of Ruby's most interesting - and useful! - features is being able to change how classes and objects behave while your app is running.

If you use Rails, then certainly you have used many runtime defined methods. Let's look at an example. Imagine a one to many association between two models: Author and Post.

If the author is already instantiated, then we can get the posts for this author by invoking

@author = Author.find(1)
@author.posts
=> #<ActiveRecord::Associations::CollectionProxy [...]>

But where does the posts method come from? This method is actually defined during runtime by the has_many method.

# app/models/author.rb

class Author < ApplicationRecord
  has_many :posts
end

When reading the author model file, Ruby will invoke the has_many methods passing the argument :posts to it. Under the hood, has_many will change the class that invoked it and add the association methods.

Alt Text

This is possible thanks to the magic of class_eval, a Ruby method designed to change the behavior of classes during runtime.

It's a remarkable mean to provide shared functionality to classes in an elegant way that feels almost like you're just changing a configuration file.

How class_eval works

There are two ways of using class_eval: passing it a block or passing it a string with the changes we'd like to execute to the class. Let's look at them separately.

Passing a block

When passing a block to class_eval, it will evaluate that block within the context of the class that is invoking it.

When passing blocks, it is very common to use define_method for defining methods during runtime. It is a method itself that

  1. Accepts the name of the new method as the argument
  2. Accepts a block to define the new method's body
  3. Yields the new method's regular arguments and block arguments to the body
define_method("name_of_new_method") do |arg1, arg2, &block|
  # Define the behavior of the new method.
  # arg1, arg2 and &block are accessible here in the body.
end

Now, let's analyze an example where we want to add a sing method to our classes. When invoked, it will define a method with the music genre for that class that just prints a string.

# Define the method sing. This method can be put
# wherever is convenient (e.g.: a module or a parent class)

def sing(genre)
  # "Open" the class for changes
  class_eval do

    # Use the define_method method to define "rock"
    # in runtime
    define_method(genre) do |band|
      puts "I love #{band}!"
    end
  end
end

class Person
  sing :rock
end

# After evaluating the class, the new rock method exists
Person.new.rock("AC/DC")
=> I love AC/DC!

We have our rock method working!

If we wanted to add methods to a class that do not depend on the argument passed to sing, then a simple def inside the class_eval block is another option.

def sing
  class_eval do
    # This def is evaluate from the context of the class
    # so this will be defined in any class that invokes
    # sing

    def rock(band)
      puts "I love #{band}!"
    end
  end
end

class Person
  # Notice the absence of the argument
  sing
end

Person.new.rock("Queen")
=> I love Queen!

Passing a string

The second manner of using class_eval is by passing it three arguments: a string with the Ruby that will be evaluated in the class, the file where it should be evaluated and the line where it should be evaluated.

These two last arguments will typically always be the same, but they can be changed to achieve other types of behavior.

The Ruby methods __FILE__ and __LINE__ are commonly used here and they return the current file and line being evaluated, respectively.

Let's use this case as an example of how we could write has_many using class_eval. Notice that this is not the real implementation that Rails uses, but the general idea is similar.

The usual manner to use the string version of class_eval is to define a Ruby string with a HEREDOC and interpolate whatever data you need.

def has_many(resource)
  # Turn current class name into name_id
  # E.g.: NewsLetter -> news_letter_id
  # Evaluated before opening class for modification

  id_attribute_name = "#{self.name.underscore}_id"

  class_eval(<<~ASSOCIATION_METHODS, __FILE__, __LINE__ + 1)
    def #{resource}
      # Turn the argument (e.g.: :posts) into the class
      # name Post
      #{resource.to_s.singularize.camelize}.where(#{id_attribute_name}: self.id)
    end
  ASSOCIATION_METHODS
end

Now let's use this in a class and understand what the HEREDOC is evaluated to.

class Author
  # Using our version of has_many
  has_many :posts
end

# In this case, the resource argument is :posts.
# The string resulting from the HEREDOC is the one below.
# Notice that this will get evaluated within the context
# of the Author class and then will define
# the instance method posts, allowing us to
# invoke
# author.posts 

def posts
  Post.where(author_id: self.id)
end

It's important to point out that many changes can be applied in a single invocation of class_eval. For instance, the real implementation of has_many in Rails would also provide the class with a setter. Both getter and setter can be defined at the same time.

@author = Author.find(1)
@author.posts = [...]

Conclusion

Using class_eval enables developers to define behavior in runtime. This technique can be used to create methods with arguments and options passed in at a class level which results in an elegant organization of code.

Have you used this before? Let me know some use cases in the comments!

Posted on by:

vinistock profile

Vinicius Stock

@vinistock

Senior Developer @ Shopify. Ruby & Rails open source contributor

Discussion

markdown guide
 

Nice post, Vini!

I've only used it once before a long time ago, to open up a class that didn't belong to me with an extension (at runtime). I generally avoid meta-programming in Ruby. It's sort of the source of "Rails magic" as some people like to call it. It does make things a lot more elegant. However, I do a ton of it in my own code in Elixir, though.

I've bookmarked this as reference should I ever reach for it again in Ruby. I've learned a couple of neat things from your post. Thank you!

Also, I know that @tenderlove just joined you guys. Have you had a chance to work with him?

 

Thank you for the kind words! I'm glad the post taught you something. I find Elixir to be super interesting, but didn't get my hands dirty with it yet.

Yes, he did join us, but we are in different teams so I haven't had the chance yet.

 

I learned about class_eval and instance_eval the hard way 🤦‍♂️
You can't avoid it when building Redmine plugins...