Building Roman Numerals in a Day with Ruby Metaprogramming
One of my favorite things about Ruby is the fact that it is a very pure Object-Oriented language and that everything is an object. This fact, combined with the fact that Ruby is also a runtime language can leave you doing some pretty cool edits to the standard library in your codebase.
In this article, I am going to briefly discuss a section of Ruby metaprogramming known as Dynamic methods. These are methods that you write for existing Ruby classes that already in the codebase of the standard library.
For this tutorial, we are working on the basis that you have Ruby installed on your machine.
From IRB
The quickest place to learn about any new concept in Ruby is by typing it into the IRB terminal. From the terminal window, type in the following:
$ irb
Now you should be in the IRB terminal. Next, we are going to add a method to the string class called William. This method just simply returns my name but it should give you an insight into what a dynamic method is.
class String
def william
return "William"
end
end
Now in the IRB terminal, we can do the following:
irb> "string".william
=> "William"
You can see from this example that a dynamic method is a method that is created at runtime. You can add another method to a class at a point when the code gets executed. Dynamic methods open a whole new paradigm when it comes to solving common coding challenges. Let’s look at how we can use dynamic methods to create a program that converts small numbers into roman numerals.
Solving the Roman Numeral Challenge Using Dynamic Methods
Programming is a mixture of art and science. Unfortunately, when it comes to coding challenges, there are many correct answers that garner fierce debate. A common challenge I see posted comes in the form of roman numerals.
How would we write a computer program that converts a number into a roman numeral using dynamic methods?
Let’s have a look at one possible solution that goes as far as 5000. From the terminal window, let’s create a new project
mkdir roman_numeral
Now we can create a new file:
touch roman_numerals.rb
I like to write tests for my code so we will install RSpec and create a spec sheet:
gem install rspec
After we have installed the RSpec gem, we can initialize Rspec
rspec --init
In our spec directory, we can create the Roman Numeral spec sheet.
touch spec/roman_numeral_spec.rb
Now we have everything ready to get the project started. Let’s start by writing lots and lots of tests. Add the following to our spec/roman_numeral_spec.rb:
require_relative "../roman_numerals.rb"
RSpec.describe Numeric, "#roman_numeral" do
context "#convert number into roman numeral" do
it "1.roman_numeral returns I" do
expect(1.roman_numeral).to eq "I"
end
it "5.roman_numeral returns I" do
expect(5.roman_numeral).to eq "V"
end
it "4.roman_numeral returns IV" do
expect(4.roman_numeral).to eq "IV"
end
it "6.roman_numeral returns IV" do
expect(6.roman_numeral).to eq "VI"
end
it "7.roman_numeral returns VII" do
expect(7.roman_numeral).to eq "VII"
end
it "8.roman_numeral returns VII" do
expect(8.roman_numeral).to eq "VIII"
end
it "9.roman_numeral returns IX" do
expect(9.roman_numeral).to eq "IX"
end
it "10.roman_numeral returns IX" do
expect(10.roman_numeral).to eq "X"
end
it "13.roman_numeral returns XIII" do
expect(13.roman_numeral).to eq "XIII"
end
it "15.roman_numeral returns XV" do
expect(15.roman_numeral).to eq "XV"
end
it "18.roman_numeral returns XVIII" do
expect(18.roman_numeral).to eq "XVIII"
end
it "19.roman_numeral returns XIX" do
expect(19.roman_numeral).to eq "XIX"
end
it "30.roman_numeral returns XXX" do
expect(30.roman_numeral).to eq "XXX"
end
it "50.roman_numeral returns L" do
expect(50.roman_numeral).to eq "L"
end
it "51.roman_numeral returns LI" do
expect(51.roman_numeral).to eq "LI"
end
it "89.roman_numeral returns LXXXIX" do
expect(89.roman_numeral).to eq "LXXXIX"
end
it "99.roman_numeral returns XCIX" do
expect(99.roman_numeral).to eq "XCIX"
end
it "145.roman_numeral returns CXLV" do
expect(145.roman_numeral).to eq "CXLV"
end
it "459.roman_numeral returns CDLIX" do
expect(459.roman_numeral).to eq "CDLIX"
end
it "1984.roman_numeral returns MCMLXXXIV" do
expect(1984.roman_numeral).to eq "MCMLXXXIV"
end
it "1545.roman_numeral returns MDXLV" do
expect(1545.roman_numeral).to eq "MDXLV"
end
it "4936.roman_numeral returns MMMMCMXXXVI" do
expect(4936.roman_numeral).to eq "MMMMCMXXXVI"
end
end
end
Now that we have some tests written, let’s dive into using dynamic methods on the Fixnum
class that will convert our numbers into Roman Numerals. You can see from our tests, we have the following example:
it "1984.roman_numeral returns MCMLXXXIV" do
expect(1984.roman_numeral).to eq "MCMLXXXIV"
end
Dynamic methods allow us to create a method that we can call on instances of the Fixnum
class. Ruby has useful methods such as to_s
which converts a number into a string. In this case, we are driving inspiration from Ruby conventions and adding a method without changing any of the Ruby libraries. We are simply adding a new method. Let’s see how this is done.
The first thing we do is define the class Fixnum. Even though there is a Ruby class called Fixnum already, doing the following will not overwrite the actual class. In our roman_numerals.rb file:
class Fixnum
end
Now we can modify the Fixnum
class by adding an additional method. To make our tests pass, we need a to_roman_numeral
method that we can call on integers. Write the following in our roman_numerals.rb file:
class Fixnum
def roman_numeral
return "The Romans have no zeros just heros. https://www.theguardian.com/notesandqueries/query/0,5753,-1358,00.html " if self == 0
symbols = {1000 => "M",900 => "CM", 500 => "D",400 => "CD", 100 => "C",90 => "XC", 50 => "L",40 =>"XL", 10 => "X",9 => "IX", 5 => "V",4 => "IV", 1=> "I"}
multiplier = self
symbol = []
count = 0
symbols.each do |num, sym|
symbol.push(sym * (multiplier/num))
multiplier = multiplier % num
count += 1
end
return symbol.join
end
end
When you run RSpec, you should see all the tests pass. From your current directory, run the following:
rspec
You’ve now seen dynamic methods in action. We can now call .to_roman_numeral
on any number, just like we .to_s
. Dynamic methods are great for little programs like this but, be warned, adding dynamic methods to your application like this can have a massive downside if you are not careful.
WARNING: Overwriting Existing Methods
The biggest potential downside with dynamic methods are overwriting existing methods in the ecosystem with your own methods. This could lead to Gems breaking, silent errors, or even overly complicated code.
Let’s take the Fixnum
class again. If we accidentally write our own .to_s
method, then we have potentially removed core, expected functionality from our application. Since the code will get executed at Runtime, we won’t see the error occur until it is called, and even then it may result in a silent error.
When it comes to dynamic methods, it pays to be additive. Instead of overwriting methods, it is safer to add on and quickly check that you are not overwriting an existing method. In conjunction with this, I highly recommend tests to cover your application so you can quickly spot when something breaks down.
Conclusion
Now that you have had a quick introduction to an aspect of Ruby MetaProgramming, how would you use it? Do you think dynamic methods are useful or are they too dangerous for one’s own good? I would love to hear your opinions in the comments below.