7 Deadly Sins of Ruby Metaprogramming

in Development

As a developer, you spend 90% of your time on code-related activities like reading and maintaining existing code. With such a large chunk of time spent on these tasks, it’s crucial to make sure everything you do (and code) is efficient. While metaprogramming with Ruby can be extremely powerful, using clever metaprogramming that makes it difficult to read or making bad tradeoffs will, ultimately, increase the cost of maintenance in the long term. So today I want to share the mistakes that can keep you from harnessing some of Ruby’s killer features in your projects.

Sin 1: Using method_missing as the very first option

Paolo Perrotta, the author of Metaprogramming Ruby book, says: “The method_missing() is a chainsaw: it’s powerful, but it’s also potentially dangerous.” The best way to use them? Sparingly and short. Let me show you an example using method_missing to implement Null Object pattern for an order, whose class will have a method to handle different currencies.

class NullOrder
  def price_euro
    0.0
  end

  def price_usd
    0.0
  end

  # ...
  # more methods needed to handle all other currencies
end

Now, in order to avoid writing more methods, we have two options — we can either use define_method or method_missing. Let’s now benchmark this using define_method and method_missing.

require 'benchmark'

iterations = 100_000

Benchmark.bm do |bm|
  bm.report('define_method') do
    class NullOrder

      ['usd', 'euro', 'yen'].each do |method|
       define_method "price_#{method}".to_sym do
         0.0
       end
      end

    end
    iterations.times do
      o = NullOrder.new
      o.price_euro
      o.price_usd
    end
  end

  bm.report('method_missing') do

    class NullOrder2
      def method_missing(m, *args, &block)
        m.to_s =~ /price_/ ? 0.0 : super
      end
    end

    iterations.times do
      o2 = NullOrder2.new
      o2.price_euro
      o2.price_usd
    end
  end
end
user system total real
define_method 0.050000 0.000000 0.050000 (0.062126)
method_missing 0.460000 0.000000 0.460000 (0.582257)

This report shows that define_method is 10 times faster than using method_missing. We also don’t need that much flexibility to handle any currency. As long as we can list the currency that will be handled in our code, we can use define_method to reduce the method duplication and achieve better performing code.

Sin 2: Not overriding respond_to_missing?

You must override the respond_to_missing? method every time you override method_missing. If you’ve worked in a Rails project, you’ll be familiar with checking the current environment by using:

Rails.env.production? instead of doing Rails.env == ‘production’

So, let’s take a look at how this is implemented in ActiveSupport string_inquirer.rb in Rails 4.2:

module ActiveSupport
  # Wrapping a string in this class gives you a prettier way to test
  # for equality. The value returned by Rails.env is wrapped
  # in a StringInquirer object so instead of calling this:
  #
  #   Rails.env == 'production'
  #
  # you can call this:
  #
  #   Rails.env.production?
  class StringInquirer < String
    private

      def respond_to_missing?(method_name, include_private = false)
        method_name[-1] == '?'
      end

      def method_missing(method_name, *arguments)
        if method_name[-1] == '?'
          self == method_name[0..-2]
        else
          super
        end
      end
  end
end

The method_missing implementation checks to make sure the method ends with a question mark. If it does, it chops that question mark off from the method name and compares it to the current object (the value of self). And if they’re the same, it returns true otherwise false.

You can see the conditional used to trap certain calls is the same as in respond_to_missing? implementation, and that’s exactly how we want it. If you don’t override respond_to_missing?, the object will not respond to any of the dynamically generated methods. This will be a surprise when developers experiment with your library in the irb console, and a good library works as expected with very few surprises, if any.

Sin 3: Forgetting to handle unknown cases

In the previous example, you can see how Rails uses super to propagate a call that the current method does not know how to handle. In the StringInquirer class above, if the method does not end with a question mark, then it allows the call to propagate further up by calling super.

If you don’t fallback to super, then it might lead you to bugs that are really hard to track. Remember, method_missing is where the bugs go to hide. So don’t forget to fallback on BasicObject#method_missing when you don’t know how to handle a call.

Sin 4: Using define_method when it’s not needed

Here is an example from Restclient gem (version 2.0.0.alpha). In the bin/restclient, you will find:

POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

POSSIBLE_VERBS.each do |m|
  define_method(m.to_sym) do |path, *args, &b|
    r[path].public_send(m.to_sym, *args, &b)
  end
end

def method_missing(s, * args, & b)
  if POSSIBLE_VERBS.include? s
    begin
      r.send(s, *args, & b)
    rescue RestClient::RequestFailed => e
      print STDERR, e.response.body
      raise e
    end
  else
    super
  end
end

Why is this a sin? Because you’re sacrificing readability and comprehension of code without gaining anything in return. The list of HTTP verbs is stable — they almost never change. But by using metaprogramming, you have increased the complexity by dynamically defining methods for the HTTP verbs. We don’t have an explosion of methods problem here, so there is no need for any metaprogramming.

Sin 5: Changing the semantics when opening classes

You should check to see if the method already exists before you open an existing class and add a method. If you don’t, you’ll change the semantics of an existing method by mistake. This will be a nasty surprise to the users of your library. So, prefer refinements over opening classes globally to reduce polluting the global namespace. A good example for this is the JSON gem. It opens the Ruby built-in classes like Range, Rational, Symbol, and so on to define the to_json method.

Sin 6:  Wrong dependency direction

In a layered architecture, the bottom-most layer could be depended upon by many other libraries, which sit on top it. So, it must be agnostic to any of the layers above to be reusable. Having the dependency direction pointing upward is wrong, and one of the horrible sins a programmer can commit. Even though it’s related to the introspective aspect of Ruby more than metaprogramming, I think the impact is huge and worth mentioning.

I’ve seen this mistake made on projects I’ve worked on with clients — libraries at the lowest layer should not use defined? some_constant to see the execution context in which it’s running to change behavior. Libraries at the lowest layer must be independent of their execution context. However, the library can provide API for customized use in a particular context. Another option is using configuration files in order to customize the behavior. The dependency should be in one direction, and should always point toward the stable abstractions.

Sin 7: Too many levels of nesting

Using metaprogramming in your code forces the client to use too many nested blocks, and unfortunately, you can see many open-source projects that use RSpec commit this sin. Here is an example from Spree gem that makes it difficult to understand the code. The following code is part of the backend/spec/controllers/spree/admin/payments_controller_spec.rb.

require 'spec_helper'

module Spree
  module Admin
    describe PaymentsController, :type => :controller do
      stub_authorization!

      let(:order) { create(:order) }

      context "order has billing address" do
        before do
          order.bill_address = create(:address)
          order.save!
        end

        context "order does not have payments" do
          it "redirect to new payments page" do
            spree_get :index, { amount: 100, order_id: order.number }
            expect(response).to redirect_to(spree.new_admin_order_payment_path(order))
          end
        end

        context "order has payments" do
          before do
            order.payments << create(:payment, amount: order.total, order: order, state: 'completed')
          end

          it "shows the payments page" do
            spree_get :index, { amount: 100, order_id: order.number }
            expect(response.code).to eq "200"
          end
        end

      end

    end
  end
end

This is a sin because it increases the context you need to reason about a particular piece of code. The simpler the API, the more elegant and easier it is to use. Good examples are validation methods of ActiveModel — here’s one from the Rails documentation for a person class:

class Person
  include ActiveModel::Validations

  attr_accessor :name
  validates_presence_of :name
end

And here’s another example — one-level nesting in a routes.rb file in Rails.

resources :articles do
  resources :comments
end

Metaprogramming is extremely valuable, and can make solving complex problems easier. But remember, it’s only worth using when there’s a tradeoff — like readability and comprehension in exchange for solving complex problems with less code. As long as you keep these tips in mind, you’ll find yourself becoming a better and more efficient developer in no time. Let me know your thoughts on metaprogramming, along with any advice you have, in the comments section below!

Code School

Code School teaches web technologies in the comfort of your browser with video lessons, coding challenges, and screencasts. We strive to help you learn by doing.

Visit codeschool.com

About the Author

Bala Paranj

Bala Paranj

Bala Paranj has a masters degree in Electrical Engineering from The Wichita State University and has been working in the IT industry since 1996. He started his career as Technical Support Engineer and became a Web Developer using Perl, Java and Ruby. You can see his work at Ruby Plus.

Might We Suggest