Ruby Metaprogramming
Ruby is a very flexible language; learn how it's metaprogramming features can work for you.
Join the DZone community and get the full member experience.
Join For FreeIn this article, we’ll be looking at a few different aspects of metaprogramming in Ruby. For starters, what is metaprogramming?
Metaprogramming is the writing of computer programs with the ability to treat programs as their data. It means that a program could be designed to read, generate, analyze, or transform other programs and even modify itself while running.
We’ll specifically look at how we can read and analyze our code in Ruby, how we can call methods (or send messages) dynamically, and how we can generate new methods during the runtime of our program.
Asking Our Code Questions
One aspect of metaprogramming that Ruby excels at is being able to ask our code questions about itself during runtime. This is otherwise known as introspection. Just like we can ask ourselves questions such as “Why am I here?”, our code can do likewise, albeit the questions may not be so existential.
Am I Able to Respond to This Method Call?
We can ask any object whether it has the ability to provide a response to a specific method call before we make it using the respond_to?
method.
"Roberto Alomar".respond_to? :downcase
# => true
"Roberto Alomar".respond_to? :floor
# => false
What Does My Object Ancestry Chain Look Like?
If you check an ActiveRecord
model in Rails 5, you’ll see that it has an astounding 71 ancestors. This includes both direct parents through class hierarchy and also modules that are included in any of the class tree. This is a bit insane and goes to show just how large of a project Rails is.
School.ancestors.size
# => 71
String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]
What Instance Variables and Methods Have Been Defined?
We can use the methods
method to give us a list of all methods available to a specific object and the instance_variables
method to give us a list of the instance variables defined/used by this object.
require 'date'
class Alpaca
attr_accessor :name, :birthdate
def initialize(name, birthdate)
@name = name
@birthdate = birthdate
end
def spit
"Putsuuey"
end
end
spitty = Alpaca.new('Spitty', Date.new(1990, 10, 10))
spitty.methods
# => [:name, :name=, :birthdate, :spit, :birthdate=, :instance_of?, :public_send, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :private_methods, :kind_of?, :instance_variables, :tap, :is_a?, :extend, :define_singleton_method, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :inspect, :display, :send, :object_id, :to_s, :method, :public_method, :singleton_method, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :trust, :untrusted?, :methods, :protected_methods, :frozen?, :public_methods, :singleton_methods, :!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
spitty.instance_variables
# => [:@name, :@birthdate]
Sending Messages
Ruby is a dynamic language. It consists of a series of objects that can pass messages back and forth among themselves. This message passing is generally what we refer to when we say “call a method.” Let’s take a look at the downcase
method of String
objects.
"Roberto Alomar".downcase
# => "roberto alomar"
When we invoke or call this method using the dot-notation, what we are really saying is that we are passing a message to the String
, and it decides how to respond to that message. In this case, it responds with a downcased version of itself.
Let’s break this down further. We have three parts we are working with: The first, "Roberto Alomar"
, is the object, the one that will receive this message. The .
(dot) tells the receiving object that we will be sending it some command or message. What follows after the dot, downcase
, is the message we are sending. In English, we could say that we are sending the downcase
message to "Roberto Alomar"
. It figures out what to do or send back once it receives that message.
In Ruby, this can be done another way, by using the send
method:
"Roberto Alomar".send(:downcase)
# => "roberto alomar"
Generally, you wouldn’t use this form in normal programming, but because Ruby allows us to send messages (or invoke methods) in this form, it gives the option of sending a dynamic message or calling methods dynamically.
method = :downcase
"Roberto Alomar".send(method)
# => "roberto alomar"
This may not seem like much, but this is one of the constructs or ideas in Ruby that allows us to write very dynamic code, code that may not even exist when you write it. In the following section, we will look at how we can generate new code dynamically in Ruby using the define_method
method.
Generating New Methods
Another aspect of metaprogramming that Ruby gives us is the ability to generate new code during runtime. We’ll do this using a method from the Module
class calleddefine_method. The way it works is by passing a symbol which becomes the name of our new method, and by providing a block, we give our new method its body. Here is simple example below:
class Person
define_method :greeting, -> { puts 'Hello!' }
end
Person.new.greeting
# => Hello!
You may have seen the delegate method before, which comes in ActiveSupport
with Rails and extends Module
. This allows us to say that when you call a certain method, you call that method on a different object rather than the current one (self
). We’re going to create a much simpler version of theirs as a way to show some metaprogramming. You can see the source code for the Rails version here.
This example is modeled after the real-life scenario of calling a business and the receptionist takes your call. You might say that the work is delegated to them.
First we will add a new method to the Module
class (which all classes have in their ancestry chain) called delegar
(the Spanish word for delegate).
class Module
def delegar(method, to:)
define_method(method) do |*args, &block|
send(to).send(method, *args, &block)
end
end
end
When this method is called, it will define a new method whose job it is to “pass off” (delegate) the work to another object, like a proxy.
class Receptionist
def phone(name)
puts "Hello #{name}, I've answered your call."
end
end
class Company
attr_reader :receptionist
delegar :phone, to: :receptionist
def initialize
@receptionist = Receptionist.new
end
end
company = Company.new
company.phone 'Leigh'
# => "Hello Leigh, I've answered your call."
You can see we call the phone
method on the Company
, but it is the Receptionist
who actually answers the call.
Dollars and Cents
You’ve probably heard that it’s bad to store and use money as a Float
because of floating point arithmetic issues. One of the ways to deal with this is to store money in cents. $10.25 would be stored in the database as 1025 cents.
Users aren’t going to want to enter things in cents though, so ideally we would have some code to help us convert between dollars and cents. We’re going to use a bit of metaprogramming to help us make things easier.
Let’s look at a class called Purchase
which has a field in the database called price_cents
. This is what the class looks like:
class Purchase
attr_accessor :price_cents
extend MoneyFields
money_fields :price
end
If this were an ActiveRecord
object in Rails, we wouldn’t have to include the line attr_accessor :price_cents
because it would do that for us, but for this example, we are just using a plain old Ruby object. This code now gives us the ability to interact with the field like so:
purchase = Purchase.new
purchase.price = 10.25
purchase.price_cents
# => 1025
purchase.price_cents = 555
purchase.price
# => #<BigDecimal:7fbc7497ac88,'0.555E1',18(36)>
But where did the methods price
and price=
come from? Our money_fields
method ends up creating these two new methods which interact with the price_cents
and price_cents=
methods that come from the attr_accessor
line or exist for us from ActiveRecord
.
module MoneyFields
require 'bigdecimal'
def money_fields(*fields)
fields.each do |field|
define_method field do
value_cents = send("#{field}_cents")
value_cents.nil? ? nil : BigDecimal.new(value_cents / BigDecimal.new("100"))
end
define_method "#{field}=" do |value|
value_cents = value.nil? ? nil : Integer(BigDecimal.new(String(value)) * 100)
send("#{field}_cents=", value_cents)
end
end
end
end
The money_fields
method loops through one or more fields which were passed to it creating reader and writer methods for the dollar form of the field. To show that it works as expected, here is a test suite that tests the different conversions back and forth:
require 'minitest/autorun'
class PurchaseTest < MiniTest::Test
attr_reader :purchase
def setup
@purchase = Purchase.new
end
def test_reading_writing_dollars
purchase.price = 5.00
assert_equal purchase.price, 5.00
end
def test_converting_to_dollars
purchase.price_cents = 500
assert_equal purchase.price, 5.00
end
def test_converting_to_cents
purchase.price = 5.00
assert_equal purchase.price_cents, 500
end
def test_writing_dollars_from_string
purchase.price = "5.00"
assert_equal purchase.price_cents, 500
end
def test_nils
purchase.price = nil
assert_equal purchase.price, nil
end
def test_creating_methods
assert_equal Purchase.instance_methods(false).sort, [:price_cents, :price_cents=, :price, :price=].sort
end
def test_respond_to_dollars
assert_equal purchase.respond_to?(:price), true
assert_equal purchase.respond_to?(:price=), true
end
end
Conclusion
Metaprogramming is fantastic but only when it is used sparingly. It can help you write repetitive code more easily (such as the money fields example), it can help you debug and analyze what your code is doing, but it also can add indirection and make it much more difficult to figure out what is actually happening in the code. Only use metaprogramming if it provides a clear advantage.
Most of the methods we’ve looked at today come from either the Object class or the Module class. Explore it more yourself!
Related Refcard:
Published at DZone with permission of , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments