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 /postsdefine_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
More in Programming Techniques