Alex Lion
Posted on August 21, 2020
Ruby is one of the underrated programming languages among modern developers. It has become popular with the Ruby on Rails framework.
Ruby is making developers happy, productive and enjoying programming. - Yukihiro Matsumoto
You are maybe coming from the JS world with a lot of frameworks or from Java and all its complexity.
If you are enough of wasting your time building software that matter and want concrete result as soon as possible, let me introduce you to Ruby.
I'll introduce you to the concept of metaprogramming.
First time I learned Ruby, my mind literally blew when I was confronted to this.
Think about a program that can generate program itself. A program that can generate executable code without the help of a developer.
No, it's not sci-fi, it's Ruby Metaprogramming !
If I had to explain to a 5 years old, imagine you want to draw a sunny city, you take your pen and write "Sunny City" on the paper. Push the button and magic happens. That's metaprogramming.
Now, I'll explain to real developers.
In Ruby, you can make runtime introspection on method. In another hand, you can ask an object about its capabilities (Do you have this method?), its variables, constants and its class and ancestors.
In this article I'll show you some examples with methods like respond_to? (? is a Ruby convention for method returning a boolean) send and define_method. But there is a lot more like method_missing, remove_method and undef_method.
I'll explain these 3 methods and finally show you the mind-blowing examples.
The respond_to?() method
This method tests your class if it can handle a specific message, for those who don't speak Ruby: it checks if a method can be called in a specific class.
(In the Ruby vocabulary, message is known as a method).
Here is a Shipment class:
class Shipment
def prepare_for_delivery
@message = 'Shipment is prepared for delivery'
end
def tracking_code(code)
@tracking_code = code
end
end
Use the respond_to? method to check if prepare_for_delivery method exists:
s = Shipment.new
s.respond_to?(:prepare_for_delivery)
==> true
A more complete example sending a message, if this one exists, to another object.
s = Shipment.new
if s.respond_to?(:cancel_shipping)
s.cancel_shipping
else
puts "Oh no ! Shipment cannot be cancel."
end
It can be used for any classes.
'hello'.respond_to?(:count)
==> true
'world'.respond_to?(:include)
==> false
Did you catch it?
Well, keep it in mind for the end of this article.
The send() method
You can call any method in a class with the send() method.
s = Shipment.new
if s.respond_to?(:tracking_code)
s.send(:tracking_code, '123ABC') # or s.send('cancel_shipping', '123ABC')
else
puts "Tracking code is not available."
end
(Message is sent in the first parameter of send())
I'm hearing you saying: "Why not calling our method directly ?"
Yes, we can and I know that this example is not a real world example on how we use it.
Let's continue.
The define_method() method
Now that you know the logic, you could even find the behavior of this method before I explain it to you.
It… defines a method ? Well done !
class Shipment
define_method :cancel do |reason|
@cancelled = true
puts reason
end
end
You just defined a new method for the Shipment class, setting an instance variable cancelled to true and printing the reason.
Wait, for the final example that will blow your mind.
Metaprogramming in one example
You know the basics of Ruby metaprogramming, let's see the final example.
Let's embark on the sea trip to metaprogramming and set sail !
# We create a container class. We only store the product's name at instantiation
class Container
attr :product_name
def initialize(name)
@product_name = name
end
end
# Apples ! Oranges ! All of theses fruits are contained in FruitContainer extending Container
# For simplification we only have one scanner
class FruitContainer < Container
def apples_scanner
puts "Scanning apples..."
end
end
# Potatoes ! Broccoli ! All of vegetables are contained in VegetableContainer extending Container
# For simplification we only have one scanner
class VegetableContainer < Container
def potatoes_scanner
puts "Scanning potatoes..."
end
end
# The Cargo containing all the containers
class Cargo
# The constructor accepting multiple parameters
def initialize(*containers)
containers.each do |container|
# self.class.send is used to define singleton methods for our class
# we could also use define_singleton_method
self.class.send(:define_method, "inspect_#{container.product_name}") do
scanner_name = "#{container.product_name}_scanner"
if container.respond_to?(scanner_name)
container.send(scanner_name)
else
puts "No scanner found."
end
end
end
end
end
potatoes_container = VegetableContainer.new "potatoes"
apples_container = FruitContainer.new "apples"
cargo = Cargo.new(potatoes_container, apples_container)
cargo.inspect_apples
cargo.inspect_potatoes
We used all the methods I explained. For each Container classes, we define new methods which call a method (if exists) based on their product name, a scanner.
The output:
Scanning apples...
Scanning potatoes...
Now, you know what metaprogramming is and how it works. Well done.
One of the first uses case of metaprogramming is creating its own DSL (Domain Specific Languages).
There are some well-known tools based on the Ruby DSL, Chef and Puppet for DevOps peoples.
This is what makes Ruby beautiful.
Since I can't offer you a book on this topic, I'm replying to all your questions and feedback. Join me on Twitter !
Posted on August 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024