Documentation/Buki/Ruby/ skills /ruby-metaprogramming

📖 ruby-metaprogramming

Use when working with Ruby metaprogramming features including dynamic method definition, method_missing, class_eval, define_method, and reflection.



Overview

Master Ruby's powerful metaprogramming capabilities to write code that writes code. Ruby's dynamic nature makes it exceptionally good at metaprogramming.

Dynamic Method Definition

define_method

class Person
  [:name, :age, :email].each do |attribute|
    define_method(attribute) do
      instance_variable_get("@#{attribute}")
    end

    define_method("#{attribute}=") do |value|
      instance_variable_set("@#{attribute}", value)
    end
  end
end

person = Person.new
person.name = "Alice"
puts person.name  # "Alice"

class_eval and instance_eval

# class_eval - Evaluates code in context of a class
class MyClass
end

MyClass.class_eval do
  def hello
    "Hello from class_eval"
  end
end

puts MyClass.new.hello

# instance_eval - Evaluates code in context of an instance
obj = Object.new
obj.instance_eval do
  def greet
    "Hello from instance_eval"
  end
end

puts obj.greet

module_eval

module MyModule
end

MyModule.module_eval do
  def self.info
    "Module metaprogramming"
  end
end

puts MyModule.info

Method Missing

Basic method_missing

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

  def method_missing(method_name, *args)
    if method_name.to_s.start_with?("find_by_")
      attribute = method_name.to_s.sub("find_by_", "")
      @data.find { |item| item[attribute.to_sym] == args.first }
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?("find_by_") || super
  end
end

users = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 }
]

finder = DynamicFinder.new(users)
puts finder.find_by_name("Alice")  # {:name=>"Alice", :age=>30}

Const Missing

class DynamicConstants
  def self.const_missing(const_name)
    puts "Constant #{const_name} not found, creating it..."
    const_set(const_name, "Dynamic value for #{const_name}")
  end
end

puts DynamicConstants::SOMETHING  # "Dynamic value for SOMETHING"

send and public_send

class Calculator
  def add(x, y)
    x + y
  end

  private

  def secret_method
    "This is private"
  end
end

calc = Calculator.new

# send can call any method (including private)
puts calc.send(:add, 3, 4)           # 7
puts calc.send(:secret_method)       # "This is private"

# public_send only calls public methods
puts calc.public_send(:add, 3, 4)    # 7
# calc.public_send(:secret_method)   # NoMethodError

Class Macros

class ActiveModel
  def self.attr_with_history(attribute)
    define_method(attribute) do
      instance_variable_get("@#{attribute}")
    end

    define_method("#{attribute}=") do |value|
      history = instance_variable_get("@#{attribute}_history") || []
      history << value
      instance_variable_set("@#{attribute}_history", history)
      instance_variable_set("@#{attribute}", value)
    end

    define_method("#{attribute}_history") do
      instance_variable_get("@#{attribute}_history") || []
    end
  end
end

class Person < ActiveModel
  attr_with_history :name
end

person = Person.new
person.name = "Alice"
person.name = "Alicia"
puts person.name_history.inspect  # ["Alice", "Alicia"]

Singleton Methods

obj = "hello"

# Define method on single instance
def obj.shout
  self.upcase + "!!!"
end

puts obj.shout  # "HELLO!!!"

# Using define_singleton_method
obj.define_singleton_method(:whisper) do
  self.downcase + "..."
end

puts obj.whisper  # "hello..."

Eigenclass (Singleton Class)

class Person
  def self.species
    "Homo sapiens"
  end
end

# Accessing eigenclass
eigenclass = class << Person
  self
end

puts eigenclass  # #<Class:Person>

# Adding class methods via eigenclass
class Person
  class << self
    def count
      @@count ||= 0
    end

    def increment_count
      @@count ||= 0
      @@count += 1
    end
  end
end

Person.increment_count
puts Person.count  # 1

Reflection and Introspection

Object Introspection

class MyClass
  def public_method; end
  protected
  def protected_method; end
  private
  def private_method; end
end

obj = MyClass.new

# List methods
puts obj.methods.include?(:public_method)
puts obj.private_methods.include?(:private_method)
puts obj.protected_methods.include?(:protected_method)

# Check method existence
puts obj.respond_to?(:public_method)       # true
puts obj.respond_to?(:private_method)      # false
puts obj.respond_to?(:private_method, true) # true (include private)

# Get method object
method = obj.method(:public_method)
puts method.class  # Method

Class Introspection

class Parent
  def parent_method; end
end

class Child < Parent
  def child_method; end
end

# Inheritance chain
puts Child.ancestors  # [Child, Parent, Object, Kernel, BasicObject]

# Instance methods
puts Child.instance_methods(false)  # Only Child's methods

# Class variables and instance variables
class Person
  @@count = 0
  def initialize(name)
    @name = name
  end
end

puts Person.class_variables        # [:@@count]
person = Person.new("Alice")
puts person.instance_variables     # [:@name]

Hook Methods

Inheritance Hooks

class BaseClass
  def self.inherited(subclass)
    puts "#{subclass} inherited from #{self}"
    subclass.instance_variable_set(:@inherited_at, Time.now)
  end
end

class ChildClass < BaseClass
end
# Output: ChildClass inherited from BaseClass

Method Hooks

module Monitored
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def method_added(method_name)
      puts "Method #{method_name} was added to #{self}"
    end

    def method_removed(method_name)
      puts "Method #{method_name} was removed from #{self}"
    end
  end
end

class MyClass
  include Monitored

  def my_method
  end
  # Output: Method my_method was added to MyClass
end

included and extended

module MyModule
  def self.included(base)
    puts "#{self} included in #{base}"
    base.extend(ClassMethods)
  end

  def self.extended(base)
    puts "#{self} extended by #{base}"
  end

  module ClassMethods
    def class_method
      "I'm a class method"
    end
  end

  def instance_method
    "I'm an instance method"
  end
end

class MyClass
  include MyModule  # Adds instance_method as instance method
end

class AnotherClass
  extend MyModule   # Adds instance_method as class method
end

DSL Creation

class RouteBuilder
  def initialize
    @routes = {}
  end

  def get(path, &block)
    @routes[path] = { method: :get, handler: block }
  end

  def post(path, &block)
    @routes[path] = { method: :post, handler: block }
  end

  def routes
    @routes
  end
end

# DSL usage
builder = RouteBuilder.new
builder.instance_eval do
  get "/users" do
    "List of users"
  end

  post "/users" do
    "Create user"
  end
end

puts builder.routes

Dynamic Class Creation

# Create class dynamically
MyClass = Class.new do
  define_method :greet do
    "Hello from dynamic class"
  end
end

puts MyClass.new.greet

# Create class with inheritance
Parent = Class.new do
  def parent_method
    "From parent"
  end
end

Child = Class.new(Parent) do
  def child_method
    "From child"
  end
end

child = Child.new
puts child.parent_method
puts child.child_method

Object Extension

module Greetable
  def greet
    "Hello!"
  end
end

obj = Object.new
obj.extend(Greetable)
puts obj.greet  # "Hello!"

# Only this instance has the method
another_obj = Object.new
# another_obj.greet  # NoMethodError

Binding and eval

def get_binding(param)
  local_var = "local value"
  binding
end

b = get_binding("test")

# Evaluate code in the binding context
puts eval("param", b)      # "test"
puts eval("local_var", b)  # "local value"

# instance_eval with binding
class MyClass
  def initialize
    @value = 42
  end
end

obj = MyClass.new
puts obj.instance_eval { @value }  # 42

TracePoint

trace = TracePoint.new(:call, :return) do |tp|
  puts "#{tp.event}: #{tp.method_id} in #{tp.defined_class}"
end

trace.enable

def my_method
  "Hello"
end

my_method

trace.disable

Best Practices

  1. Use metaprogramming sparingly - it can make code hard to understand
  2. Always implement respond_to_missing? when using method_missing
  3. Prefer define_method over class_eval when possible
  4. Document metaprogramming heavily - it's not obvious what's happening
  5. Use public_send over send to respect visibility
  6. Cache metaprogrammed methods to avoid repeated definition
  7. Test metaprogrammed code thoroughly - bugs can be subtle

Anti-Patterns

❌ Don't overuse method_missing - it's slow and hard to debug ❌ Don't use eval with user input - major security risk ❌ Don't metaprogram when simple code works - clarity over cleverness ❌ Don't forget to call super in method_missing ❌ Don't create methods without documenting them - IDE support breaks

Common Use Cases

  • ORMs (ActiveRecord) - Dynamic finders, associations
  • DSLs - Route definitions, configurations
  • Decorators - Method wrapping and enhancement
  • Mocking/Stubbing - Test frameworks
  • Attribute definition - Custom accessors with behavior

Related Skills

  • ruby-oop - Understanding classes and modules
  • ruby-blocks-procs-lambdas - For callbacks and dynamic behavior
  • ruby-gems - Many gems use metaprogramming extensively