🔮 Ruby Metaprogramming: A Practical Guide
Metaprogramming is one of Ruby’s most powerful features — it allows programs to modify themselves while running. Let’s explore essential metaprogramming techniques and tools, including method_missing
, eigenclasses, singleton methods, and more.
🧠 Core Concepts of Metaprogramming
method_missing
Ruby allows you to intercept calls to undefined methods using method_missing
. This enables dynamic method handling:
class Ghost
def method_missing(method_name, *args)
puts "You tried to call #{method_name}, but it doesn't exist!"
end
end
Ghost.new.hello # => "You tried to call hello, but it doesn't exist!"
const_missing
Similar to method_missing
, Ruby also lets you handle undefined constants using const_missing
:
class Object
def self.const_missing(name)
puts "Undefined constant: #{name}"
end
end
FooBar # => "Undefined constant: FooBar"
undef_method
vs remove_method
-
undef_method
disables a method in the current class and any ancestor. -
remove_method
removes it from the class, but if it exists in a superclass, it’s still accessible.
class Parent
def hello; puts "Hello from Parent"; end
end
class Child < Parent
remove_method :hello
end
Child.new.hello # => "Hello from Parent"
class AnotherChild < Parent
undef_method :hello
end
AnotherChild.new.hello # => Error: undefined method `hello`
Kernel#local_variables
You can inspect the current local scope:
my_var = 42
p local_variables # => [:my_var]
🔒 Scope Gates
Ruby introduces scope gates with constructs like class
, module
, and def
. These gates close over local variables:
my_var = 123
MyClass = Class.new do
puts "#{my_var} in the class definition!" # => Works
define_method :my_method do
puts "#{my_var} in the method!" # => Also works, due to define_method being a block
end
end
Regular def
wouldn’t have access to my_var
, but define_method
does because it's a closure.
🧱 Block, Proc, Lambda and Arity
Arity
A function’s arity is the number of arguments it expects.
p = Proc.new { |a, b| [a, b] }
puts p.arity # => 2
Lambda has stricter arity checking than Proc.
🔁 Revisiting Method
You can extract methods and reuse them like objects
class MyClass
def initialize(value)
@x = value
end
def my_method
@x
end
end
obj = MyClass.new(1)
m = obj.method(:my_method)
puts m.call # => 1
unbind
and remind methods
unbound = m.unbind
new_obj = MyClass.new(42)
puts unbound.bind(new_obj).call # => 42
🧬 Singleton Methods
Ruby allows you to define methods only on a single object, known as singleton methods.
str = "hello"
def str.shout?
self == self.upcase
end
puts str.shout? # => false
puts str.singleton_methods # => [:shout?]
⚠️ Deprecation Helpers
You can create your own deprecation system using metaprogramming:
class Book
def title
"1984"
end
def self.deprecate(old_method, new_method)
define_method(old_method) do |*args, &block|
warn "Warning: #{old_method}() is deprecated. Use #{new_method}()"
send(new_method, *args, &block)
end
end
deprecate :GetTitle, :title
end
Book.new.GetTitle
# => Warning: GetTitle() is deprecated. Use title()
# => "1984"
🪞 Understanding the Eigenclass
The eigenclass (also known as the singleton class or metaclass) allows you to define behavior specific to one object.
To explore more on this advanced topic, check out this Viblo article on eigenclass.
🧩 Conclusion
Metaprogramming in Ruby is both powerful and elegant. It allows you to:
-
Dynamically define or intercept methods.
-
Modify or remove existing behaviors.
-
Work with closures and evaluate code in flexible scopes.
Used wisely, it can make your code flexible and expressive. Used recklessly, it can make things unpredictable. Like any power tool — handle with care.