When to Use Metaprogramming

22 Apr 2021

The answer is deceptively simple: When writing framework code. Ruby on Rails uses metaprogramming to make nice things like the ‘params’ method which has a familiar hash-map like set of syntax (params[:user_id] does exactly what one would expect) and security from forbidden parameters like ‘enable_self_destruct.’ Actions that happen all over the code can benefit from some of ye old meta-programming.

Of course, anyone who’s spent anytime with a language that encourages metaprogramming has had some bad experiences with overly fancy code that trades readability for… less repeated code? modularity? Reasons lost to time? Once I cornered the lead developer of a project and made him tell me why the application connects to the database in two different ways. One was EJBs (Enterprise Java Beans - this project had been started in the late 90s) and the other was a reflection-heavy from-scratch Object Relational Mapping (ORM) framework. Now metaprogramming in early Java was not pretty, easy, or fast. So why did the team write their own ORM? There was a requirement from on high that certain parts of the application should be separately deployable. EJBs 1.0 could not handle such a thing and how hard could it be to write an ORM (A joke but, I have to say, the opportunity to build an ORM from scratch at work is pretty compelling)? Guess how many times this feature was used. That’s right: Never. This all led to a 2004 Jake having to grok two pretty different ORMs depending on what area of the code he was in. Sorry past Jake, at least you learned a lot.

How does one know if metaprogramming is appropriate or a newbie trap and can the answer be both? I believe I have an example that I worked with my team to write: The Searchable module. In our application search and sort are a huge part of every model we present to the user. Finding a fund based on its partially remembered name or sorting accounts by rate of return are just two examples of the vast number of combinations of search and sort in our application. At some point we realized that most of our controllers had some very similar code dealing with search/sort and thus we created the Sortable and Searchable modules. (I’m mostly going to focus on the Searchable module in this article) An important side note about metaprogramming: It has much less chance of being terrible if you extract a thing you repeat rather than anticipate repetition. Perhaps the sales folk swore on a swimming pool of sacred texts that this new feature would be the ticket to unicorn town and propel sales to unheard of heights. It is still best to wait and see if that feature really takes off before adding the complication of metaprogramming.

The controller implementation is terse but readable:

class SomeController < ApplicationController
  def index
    ...
    @models = AnActiveRecordModel.
                search_with( params ).
                sort_with( params ).
                paginate( page: params[:page], per_page: params[:rows] )
    ...
  end
end

The model interface also turned out pretty decent:

class AnActiveRecordModel < ApplicationRecord
  include Searchable, Sortable
  search_by name: :name_like
  custom_sort_by parent_name: :sort_by_parent_name

  def self.name_like name_fragment
    where( "LOWER( #{table_name}.name ) LIKE ?",
           "%#{name_fragment.downcase}%" )
  end

  def self.sort_by_parent_name sort_order
    joins( :parent ).
      order( "parents.name" => sort_order )
  end
end

What I like about the above code is that a programmer new to the project can follow the intent without an awful amount of trouble. If there is a problem searching by ‘name’ they could see that ‘name’ is the key and ‘name_like’ is the value in a hash map that is passed into the ‘search_by’ method. A search for ‘name_like’ yields, lo and behold, a method by that name that does searching. This hypothetical programmer probably doesn’t even have to look at the Searchable module to know what’s going on and how to interact with the “searching framework.”

As for the actual contents of Searchable, there’s some boring setup code but the real action is in this ‘reduce’ at the core of the search_with method:

  search_map( params ).reduce( self ) do |ar_class, param_key_and_value|
    param_key, search_value = param_key_and_value

    raise SearchSpecificationError,
      "'#{param_key}' is not a searchable attribute.\n" \
      "If you intended it to be, add :#{param_key} to the search_by\n" \
      "declaration in #{self.name}" unless searchable? param_key

    search_method = search_key_to_search_method param_key
    if search_value.respond_to? :has_key?
      ar_class.send search_method, search_value['min'], search_value['max']
    else
      ar_class.send search_method, search_value
    end
  end
  # Followed by a bunch of error handling

The ‘search_map’ returns the params map with a little extra fanciness in there so we can do min/max or bounded searches. In the case of only searching by name, the map might be {"name" => "ount Dracul"} so param_key would be assigned “name” and search_value would be “ount Dracul”. In this case, ‘self’ is the ActiveRecord class the Searchable module has been mixed into. From there, the code looks up the method by which to search with the help of search_key_to_search_method which uses the map that was passed into the model via the search_by class method. If the search is bounded, the min and max are extracted and passed to the appropriate method with the mighty ‘send’. Otherwise, it’s a more straightforward use of ‘send’ to call the right search method with the right value. If there were another key value pair, then the loop would continue chaining up the calls on the ActiveRecord class.

This code does use the ‘reduce’ and ‘send’ methods which are a bit daunting for many developers. However, we can still make efforts to increase readability. Good naming is essential if you want people (including the author in 6 months) to have a chance in hell of understanding a given piece of metaprogramming. Many times I’ve searched through some framework code only to discover a reduce {|m,o| ... and was crazy confused and frustrated. Really, ‘m’ for memo and ‘o’ for object? Was the dev trying to keep a secret?

The ‘raise’ in the middle of the reduce loop is another important part of metaprogramming and frameworks: Fail nice! No one wants some cryptic error from deep inside a patch of wild code. When writing a framework, validating inputs and setup are quite import to catch improper use early.

As for documentation, of course the creator should write some nice comments but… Well, things change and comments are kind of a lie waiting to happen. I prefer documentation that executes AKA automated tests. Write them or risk tormenting future developers.

One more thing: It wouldn’t be hard to make this module not require the ‘search_by’ configuration method in the model. The code could assume that any key/column ‘x’ has a corresponding ‘x_like’ method for searching. And yet, having the map at the top of the file very much gives a struggling developer some clues as to how things work. Yes, Rails prefers ‘convention over configuration’ but it has copious online documentation so in my niche application I prefer to do slightly more typing in order to keep things clear.