..

Metaprogramming

Definition

Programs that generate programs

Metaprogramming may allow developers to be more productive by reducing the amount of code that needs to be written manually. It is worth to notice that the way it is built affects (a lot) the productivity.

The two main use cases are:

  • Exposing internal execution mechanisms to code as APIs;
  • Dynamic text expression execution that contains programming commands.

Examples

Base code generation

#! /bin/sh
echo '#! /bin/sh' > program
for i in $(seq 992)
do
  echo "echo $i" >> program
done
chmod +x program

C/C++ macros

#define RADTODEG(x) ((x) * 57.29578)

Python eval/exec

>>> eval("5 ** 7")
# 78125

>>> program = 'a = 5\nb = 10\nprint("Sum = ", a+b)'
>>> exec(program)
# Sum =  15

In ruby

The Ruby language offers by default multiple ways to “metaprogram”. Here are some real world examples:

Identifying not implemented methods

Useful for generating infinity methods to classes

APIs

class Roman
  ROMAN_TO_INT = {
    i: 1,
    v: 5,
    x: 10,
    l: 50,
    c: 100,
    d: 500,
    m: 1000
  }.freeze

  def roman_to_int(str)
    # This implementation does not matter :p
    numbers = str.downcase.chars.map { |char| ROMAN_TO_INT[char.to_sym] }.reverse
    numbers.inject([0, 1]) do |result_number, int|
      result, number = result_number
      int >= number ? [result + int, int] : [result - int, number]
    end.first
  end

  private

  def method_missing(method_name)
    str = method_name.to_s
    roman_to_int(str)
  end
end

roman = Roman.new
roman.iii # => 3
roman.cix # => 109
# activesupport/lib/active_support/string_inquirer.rb
# frozen_string_literal: true

require "active_support/core_ext/symbol/starts_ends_with"

module ActiveSupport
  # Wrapping a string in this class gives you a prettier way to test
  # for equality. The value returned by <tt>Rails.env</tt> is wrapped
  # in a StringInquirer object, so instead of calling this:
  #
  #   Rails.env == 'production'
  #
  # you can call this:
  #
  #   Rails.env.production?
  #
  # == Instantiating a new StringInquirer
  #
  #   vehicle = ActiveSupport::StringInquirer.new('car')
  #   vehicle.car?   # => true
  #   vehicle.bike?  # => false
  class StringInquirer < String
    private
      def respond_to_missing?(method_name, include_private = false)
        method_name.end_with?("?") || super
      end

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

Dynamically creating methods

Useful mainly when we need to generate methods from a given list

APIs

# activerecord/lib/active_record/store.rb#L135
# ...
keys.each do |key|
  accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}"

  define_method("#{accessor_key}=") do |value|
    write_store_attribute(store_attribute, key, value)
  end

  define_method(accessor_key) do
    read_store_attribute(store_attribute, key)
  end

  define_method("#{accessor_key}_changed?") do
    return false unless attribute_changed?(store_attribute)
    prev_store, new_store = changes[store_attribute]
    prev_store&.dig(key) != new_store&.dig(key)
  end

  define_method("#{accessor_key}_change") do
    return unless attribute_changed?(store_attribute)
    prev_store, new_store = changes[store_attribute]
    [prev_store&.dig(key), new_store&.dig(key)]
  end

  define_method("#{accessor_key}_was") do
    return unless attribute_changed?(store_attribute)
    prev_store, _new_store = changes[store_attribute]
    prev_store&.dig(key)
  end
# ...
module Loggable
  def logger
    @logger ||= default_logger
  end

  private

  def default_logger
    Rails.logger
  end

  %i[info warn debug error].each do |severity|
    define_method severity do |message, *args|
      data = args.first || {}
      data[:class_name] = self.class.name unless data.key?(:class_name)
      logger.send(severity, message, data)
    end
  end
end

Dynamic method calls

Usually for composing method calls with external input

APIs

class Book
  attr_accessor :title, :author, :length

  def assign_values(values)
    values.each_key do |key|
      send("#{key}=", values[key])
    end
  end
end

book_info = {
  title: 'Forrest Gump',
  author: 'Winston Groom',
  length: 300
}

book = Book.new

# vvvvv Not necessary vvvvv
# book.title = book_info[:title]
# book.author = book_info[:author]
# book.length = book_info[:length]

book.assign_values(book_info)

Conclusion

Metaprogramming can become pretty natural and easy to use, that’s when the danger comes, with ultra generic classes and poor understandable code. Take a look at Martin Fowler’s article Is a Ruby Code-base Hard to Understand?