Programming Techniques

Metaprogramming

Code that reads, generates, or transforms other code at runtime or load-time.

Overview

Metaprogramming is the practice of writing code that treats other code as data, reading it, generating it, or modifying it at runtime, compile-time, or load-time. It allows programs to adapt their own behaviour without manual intervention, reducing boilerplate and enabling highly expressive APIs.

Origin

The concept originates from LISP in the late 1950s, where code and data shared the same structure (homoiconicity). It became a mainstream technique through languages like Smalltalk, Ruby, and Python, each of which exposed their own object model for manipulation at runtime.

Examples

Dynamic method definition in Ruby

class API
  RESOURCES = [:users, :posts, :comments]

  RESOURCES.each do |resource|
    define_method("find_#{resource}") do |id|
      http_get("/#{resource}/#{id}")
    end

    define_method("create_#{resource}") do |attrs|
      http_post("/#{resource}", attrs)
    end
  end
end

api = API.new
api.find_users(42)    # => GET /users/42
api.create_posts({})  # => POST /posts

define_method generates methods at class load time. The alternative, writing each method by hand, produces identical behaviour but six times the lines and three times the maintenance surface.

method_missing for proxy objects

class FlexibleRecord
  def initialize(data)
    @data = data
  end

  def method_missing(name, *args)
    key = name.to_s.delete_suffix('=')
    if name.to_s.end_with?('=')
      @data[key] = args.first
    elsif @data.key?(key)
      @data[key]
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @data.key?(name.to_s.delete_suffix('=')) || super
  end
end

record = FlexibleRecord.new({ 'name' => 'Jarred' })
record.name         # => "Jarred"
record.name = 'J'  # sets @data['name']

Always override respond_to_missing? alongside method_missing. Omitting it breaks duck-typing checks across the codebase.

Use Cases

  • 01Generating repetitive CRUD methods for API clients without code duplication
  • 02Building DSLs, Rails routes, RSpec matchers, and ActiveRecord associations are all metaprogramming
  • 03Serialisers and ORMs that map database columns to attributes without explicit declarations
  • 04Mocking frameworks that intercept method calls at runtime
  • 05Aspect-oriented concerns like logging, caching, and timing without touching the original method body

When Not to Use

  • //When the same result can be achieved with a simple loop or configuration object, explicitness is usually preferable
  • //In performance-critical hot paths: method_missing is significantly slower than defined methods
  • //When the team is unfamiliar with the technique, dynamic dispatch makes stack traces harder to follow
  • //In library code that must be readable by non-authors; prefer explicit declaration over implicit generation unless the DSL is well-documented

Technical Notes

  • In Ruby, define_method creates closures and captures outer variables; be careful about variable mutation in loops (use each with a block parameter, not a shared variable)
  • method_missing adds a call to the full method lookup chain. Profile before using it in high-throughput paths
  • JavaScript Proxy objects offer similar capability: trapping get, set, and apply on any object
  • In compiled languages (Go, Rust), metaprogramming typically happens at compile time via macros or code generators rather than at runtime
  • Document dynamically generated APIs thoroughly, tools like YARD in Ruby and JSDoc in JavaScript do not see methods that do not exist in source