A Little Taste of Ruby’s Metaprogramming

Saad Azam
CodeX
Published in
5 min readOct 23, 2024
Photo by Joshua Fuller on Unsplash

If you have seen some Ruby on Rails code, have you ever wondered how certain keywords are meant to work? As an example, take a look at the code below:

class User < ApplicationRecord
has_many :posts, dependent: :destroy
end

By writing this one has_many line, Rails automatically queries out all the posts of a certain user when we write user.posts. How does it work? Is it a built-in feature of Ruby (it isn’t)? And this is not the only example. Many gems also have certain ‘keywords’ that add more functionality to your Ruby/Rails projects. For example, if you have used Devise gem for authentication of a user, writing devise :database_authenticatable, :registerable,... and including other modules for the user model automatically configures authentication and other features for users. If you have used ancestry gem, declaring has_ancestry in your model configures it to have a tree-like structure and you can query that, while you have barely written any code. And the list of such examples is non-exhaustive.

So, how are these DSL-like features meant to work? How can we create our own certain keywords like these? The answer lies in Ruby’s powerful Metaprogramming.

Metaprogramming!

Metaprogramming is a technique in which computer programs can write and manipulate other programs. Ruby is a super flexible programming language and its ecosystem of metaprogramming is extremely powerful. If it wasn’t for Ruby’s metaprogramming, we would never have had Ruby on Rails.

Ruby is a pure OOP-based language. Everything in Ruby is an Object (even Class itself is an Object! You can even create a class with Class.new!). Furthermore, Ruby makes it easy for you to write singleton methods for your objects as well. Here is an example:

cool_str = 'hello'

def cool_str.cool_method
'I have a cool method'
end

not_cool_str = 'world'

puts cool_str.cool_method # Prints 'I have a cool method'
puts not_cool_str.cool_method # Shows an 'undefined method' error.

In the code above, we declared a singleton method for our cool_str and it works. We can attach such singleton methods to our classes and other certain objects. For our classes, these singleton methods are also called Class Methods (Note: We cannot define such methods for all types of objects. As an exercise, try attaching such a singleton method to an integer like we did to our cool_str). Furthermore, Ruby’s code blocks are also a powerful feature.

Features like defining class methods, usage of blocks, symbols etc., showcase Ruby’s expressiveness and its ability to write dynamic, concise and metaprogrammed code. Below is a little taste of Ruby’s metaprogramming.

Let’s Get Started

Let’s declare a base class called Animal. This class is going to have some class methods that define instance methods on runtime. These class methods can then be utilized by the child classes that are inherited from Animal.

Here is our Animal class:

class Animal
def self.can_bark
define_method(:woof) { 'Woof Woof' }
end

def self.can_meow
define_method(:meow) { 'Meow' }
end

def self.can_speak(speak_text:)
define_method(:speak) { speak_text }
end
end

Our parent Animal class has three class methods. Each of these class methods can dynamically create a new instance method for the class which is inherited from Animal. The define_method here is key. It is used to define methods in runtime. The provided block becomes the body of the method. Now, for example, if the inheriting class has the keyword can_bark, it’s instance will have a method named woof defined for it. Let’s try it out:

class Dog < Animal
can_bark
end

class Cat < Animal
can_meow
end

class UniqueAnimal < Animal
can_bark
can_meow
can_speak speak_text: 'Abrakadabra'
end

dog = Dog.new
cat = Cat.new
unique_animal = UniqueAnimal.new

puts dog.woof # Prints 'Woof Woof'
puts cat.meow # Prints 'Meow'

puts unique_animal.speak # Prints 'Abrakadabra'
puts unique_animal.woof # Prints 'Woof Woof'
puts unique_animal.meow # Prints 'Meow'

puts dogs.meow # Will render an 'undefined method' error.

Here, we declared three child classes inherited from Animal which utilize the class methods from their parent — Animal. For instance, Dog class has can_bark which would define an instance method woof for it — and all of this is done in our class method self.can_bark of Animal. We have all three ‘keywords’ (class methods) for our UniqueAnimalcan_bark, can_meow, can_speak and these class methods have declared the respective instance methods for our UniqueAnimal class, and our unique_animal expresses that behavior! You can see how these ‘magic words’ can mask the logic and define custom methods for our classes, keeping the code expressive and concise.

Lets take a look at another example.

class Attributer
def initialize(**attribute_inputs)
set(**attribute_inputs)
end

def has_attribute?(attribute)
self.class.has_attribute?(attribute)
end

def attributes
self.class.attributes
end

def set(**attribute_inputs)
attribute_inputs.each do |key, value|
next unless attributes.include?(key)

instance_variable_set("@#{key}", value)
end
end

@@all_attributes = []

def self.has_attribute(attribute_name)
raise 'Attribute can be only be String or a Symbol!' unless [Symbol, String].include?(attribute_name.class)

attribute_name = attribute_name.to_sym

attr_accessor attribute_name

@@all_attributes = (@@all_attributes << attribute_name).uniq
end

def self.has_attributes(*attributes)
attributes.each { |attribute| has_attribute attribute }
end

def self.has_attribute?(attribute)
@@all_attributes.include?(attribute.to_s.to_sym)
end

def self.attributes
@@all_attributes
end
end

This is going to be our parent class Attributer. Let’s dissect it:

  • The instance methods has_attribute? and attributes call the respective class methods: self.has_attribute? and self.attributes. These class methods check for existence of an attribute or enlist all the attributes respectively. As you can see, it is possible to call class methods in our instance methods with self.class giving us access to our class’s blueprint.
  • The class methods are also simple. The has_attribute class method takes the incoming argument (attribute) and converts it into an attr_accessor for our class, and appends that attribute to our class variable @@all_attributes. The has_attributes class method executes has_attribute on each of its argument.
  • The instance method set loops over the arguments hash and sets the values of attributes with the help of instance_variable_set — a method provided by the parent of all Objects in Ruby — Object. This set method can also be used during initialization of the object as well.

Now, let’s utilize this class:

class GameCharacter < Attributer
has_attributes :first_name, :last_name, :age, :game

def full_name
"#{first_name} #{last_name}"
end
end

character = GameCharacter.new(first_name: 'Shermie', age: 21, game: 'King of Fighters \'97')

puts "Full name: #{character.full_name}" # Prints 'Shermie'
puts "Has date_of_birth? #{character.has_attribute?(:date_of_birth)}" # Prints false
puts "Has first_name? #{character.has_attribute?(:first_name)}" # Prints true
puts "Available Attributes: #{character.attributes}" # Prints all the available attributes

character.first_name = 'Blaze'
character.last_name = 'Fielding'
character.set(age: 33, game: 'Streets of Rage 4')

puts "Character's full name: #{character.full_name}" # Prints 'Blaze Fielding'
puts "Character's game: #{character.game}" # Prints 'Streets of Rage 4'

Our GameCharacter class inherits from Attributer class and has access to all of its class methods. By just one line — has_attributes — we made all of the mentioned attributes as the attr_accessor and all the instance methods work as expected too! And this is just a basic example of metaprogramming. With all such features that Ruby provides, you can go absolutely bonkers and take it to the next level. But be careful, while all of this is super powerful, making your code secure and maintainable is also important and a skill of its own.

In Conclusion, Ruby is one of the best programming languages when it comes to metaprogramming. It’s expressiveness and flexibility is unmatched, and allow you to take your implementation of Classes to next level. And we have barely scratched the surface of how awesome the Ruby’s metaprogramming ecosystem is!

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

CodeX
CodeX

Published in CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Saad Azam
Saad Azam

Written by Saad Azam

(Currently) Ruby on Rails developer

No responses yet

Write a response