Learn Ruby - Metaprogramming & Internals
Series/Learn Ruby/Episode 19
Episode 19 of 23

Learn Ruby - Metaprogramming & Internals

This episode covers Ruby metaprogramming: send, define_method, method_missing and respond_to_missing?, instance_eval and class_eval for building DSLs, then internals like the singleton class, the method lookup chain, and hooks for plugin architecture.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Most of Ruby's power comes from its dynamism: programs that can write programs. Metaprogramming is the technique of creating methods, modifying objects, and defining behavior at runtime. Episode 19 explains these techniques and the internals behind them.

After episode 18, you know how to add types. Now we learn the flexibility that makes frameworks like Rails and Sinatra so expressive — from send and define_method to DSLs that turn configuration into code.

Basic Metaprogramming Techniques

send and define_method

send calls a method dynamically — the method name can be a string or symbol only known at runtime. define_method creates a new method on a class:

Rubysend dan define_method
class Klien
  def panggil(nama, *args)
    send(nama, *args)
  end
 
  def self.buat_getter(attr)
    define_method(attr) { instance_variable_get("@#{attr}") }
  end
end

send(nama, *args) calls the method named nama with any arguments. define_method(attr) creates an automatic getter for each attribute — a pattern used to avoid repetitive code. Careful: never call send with a method name taken from user input without a whitelist.

method_missing and respond_to_missing?

When a method isn't found, Ruby calls method_missing. This is the foundation of many DSLs — an object can catch calls it doesn't recognize:

Rubymethod_missing untuk DSL
class Pengaturan
  def method_missing(nama, *args)
    if nama.to_s.end_with?("=")
      instance_variable_set("@#{nama.to_s.chop}", args.first)
    else
      super
    end
  end
 
  def respond_to_missing?(nama, include_private = false)
    nama.to_s.end_with?("=") || super
  end
end

def method_missing(nama, *args) catches calls like pengaturan.port = 3000. respond_to_missing? must be overridden so that respond_to? also recognizes these dynamic methods — without it, object protocols such as serialization can break.

instance_eval and class_eval for DSLs

instance_eval evaluates a block with self set to the object itself, so methods and instance variables are directly accessible. This is how frameworks make configuration like config.port = 3000 feel like a DSL:

RubyDSL dengan instance_eval
class Server
  def initialize(&block)
    instance_eval(&block)
  end
 
  def port(value)
    @port = value
  end
 
  def workers(value)
    @workers = value
  end
end
 
server = Server.new do
  port 3000
  workers 4
end

Server.new do ... end executes the block within the instance context, so port 3000 becomes a method call rather than an assignment. class_eval works similarly at the class level. DSLs like this make your APIs expressive and close to human language.

Internals: Singleton Class and Method Lookup

Singleton Class

Every Ruby object has a singleton class — an invisible class that holds methods specific to that object. Methods defined with def self. or def obj.method live here:

RubySingleton method dan singleton class
server = Server.new { port 8080 }
def server.ping
  "pong"
end
 
puts server.singleton_class.ancestors.first

def server.ping adds a method to a single object only. server.singleton_class accesses that hidden class. Understand the singleton class because it helps determine how Ruby looks up methods.

Method Lookup Chain

When you call a method, Ruby walks the chain: the object's singleton class, then its class, included modules, parent classes, and so on up to BasicObject. This order determines which method wins:

RubyUrutan lookup method
puts Server.ancestors
puts server.singleton_class.ancestors

Server.ancestors prints the class and module chain from top to bottom. prepend inserts a module at the front so its methods win over the class. Understanding this order is the key to predicting behavior in code that uses mixins heavily.

Hooks and Plugin Architecture

Hooks are methods Ruby calls when certain events occur — for example when a module is included (included) or when a new method is added (method_added). Combining hooks and metaprogramming builds plugin architectures:

RubyHook included untuk plugin
module Audit
  def self.included(base)
    base.extend(ClassMethods)
    puts "#{base} memakai plugin Audit"
  end
 
  module ClassMethods
    def catat_aksi
      puts "aksi dicatat"
    end
  end
end
 
class Transaksi
  include Audit
end

module Audit has its included hook called when it is mixed into a class; base.extend(ClassMethods) turns the module's methods into class methods. Plugins written with this pattern can add behavior, validate, or log activity without manually modifying the target class.

Warning

Metaprogramming is a double-edged sword. Use it to build expressive APIs, but keep the code readable and debuggable. Less magic is better, unless that magic genuinely simplifies usage.

Conclusion

Key takeaways:

  • send calls a method with a dynamic name at runtime.
  • define_method creates new methods programmatically.
  • method_missing catches unrecognized calls and must be paired with respond_to_missing?.
  • instance_eval builds DSLs by executing a block in the object's context.
  • The singleton class holds methods specific to one object and sits first in the lookup chain.
  • Server.ancestors shows the method lookup order that determines the winner.
  • Hooks like included and method_added form the foundation of plugin architectures.

In episode 20 we guard quality: testing with Minitest and RSpec complete with mocks, stubs, and fixtures, the TDD workflow, then RuboCop for code style and SimpleCov for coverage. After learning to write magic code, we learn to write it with discipline.

Learn Ruby - Metaprogramming & Internals | Learn Ruby