Ruby
Article

Building Roman Numerals in a Day with Ruby Metaprogramming

By William Kennedy

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.

  • http://new2code.com/ William Kennedy

    Good point and that is the approach I would usually take in production apps(particularly when chaining methods together). In this case, we have defined a method that doesn’t exist into the class which can be useful and I thought it would be a nice way to solve common coding problem that gets asked. It also introduces people to a more advanced part of the Ruby language without risking too much. I will see about writing a tutorial on chaining methods together. It is a good idea.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.