Overview
The prepend
method inserts a module into the method lookup chain before the class that calls it, allowing modules to override existing methods rather than extend them. Unlike include
, which places modules after the class in the lookup hierarchy, prepend
positions modules at the front, making their methods take precedence over the class's own implementations.
When Ruby resolves method calls, it searches the prepended modules first, then the class itself, then included modules, and finally the superclass chain. This positioning makes prepend
particularly valuable for method decoration, aspect-oriented programming, and creating wrapper functionality around existing behavior.
module Greeter
def hello
"Hello from module"
end
end
class Person
prepend Greeter
def hello
"Hello from class"
end
end
person = Person.new
person.hello
# => "Hello from module"
The prepend mechanism modifies the class's ancestor chain by inserting an anonymous singleton class containing the module's methods. This singleton class becomes the first entry in the method resolution order, ensuring prepended methods execute before the class's original implementations.
Person.ancestors
# => [Greeter, Person, Object, Kernel, BasicObject]
Ruby's prepend functionality serves three primary use cases: method decoration where modules wrap existing functionality, method interception for logging or validation, and framework extensions that modify behavior without inheritance.
Basic Usage
The prepend
method accepts module arguments and inserts them into the method lookup chain in reverse order. Multiple modules prepended in a single call appear in the ancestor chain with the last argument taking highest precedence.
module Authentication
def save
return false unless authenticated?
super
end
end
module Validation
def save
return false unless valid?
super
end
end
class User
prepend Authentication, Validation
def save
puts "Saving user to database"
true
end
private
def authenticated?
true
end
def valid?
true
end
end
user = User.new
user.save
# => Saving user to database
# => true
User.ancestors
# => [Authentication, Validation, User, Object, Kernel, BasicObject]
Prepended modules gain access to the original implementation through super
, enabling method decoration patterns. The super
keyword calls the next method in the lookup chain, which may be the class's method or another prepended module.
module Timestamped
def initialize(*args)
super
@created_at = Time.now
end
end
class Document
prepend Timestamped
def initialize(title)
@title = title
end
end
doc = Document.new("Ruby Guide")
# Document now has @title and @created_at instance variables
Class methods can also be prepended using the prepend
method on the singleton class. This pattern requires accessing the class's eigenclass to modify class method resolution.
module ClassLogging
def new(*args)
puts "Creating new instance with: #{args.inspect}"
super
end
end
class Product
class << self
prepend ClassLogging
end
def initialize(name)
@name = name
end
end
Product.new("Widget")
# => Creating new instance with: ["Widget"]
The order of module prepending matters significantly. Ruby processes prepend arguments from left to right, but the method lookup searches from right to left in the argument list. Later arguments in a single prepend call take precedence over earlier ones.
module A
def test; puts "A"; super; end
end
module B
def test; puts "B"; super; end
end
class Example
prepend A, B
def test; puts "Example"; end
end
Example.new.test
# => B
# => A
# => Example
Advanced Usage
Method aliasing combined with prepend creates sophisticated decoration patterns. Modules can store references to original implementations before defining wrapper methods, enabling complex behavioral modifications.
module Memoization
def self.prepended(base)
base.extend(ClassMethods)
end
module ClassMethods
def memoize(*method_names)
method_names.each do |method_name|
original_method = instance_method(method_name)
define_method(method_name) do |*args|
@memoized ||= {}
key = [method_name, args]
@memoized[key] ||= original_method.bind(self).call(*args)
end
end
end
end
end
class Calculator
prepend Memoization
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
memoize :fibonacci
end
calc = Calculator.new
calc.fibonacci(40) # First call calculates
calc.fibonacci(40) # Second call returns cached result
Conditional method modification allows modules to alter behavior based on runtime conditions or configuration. This pattern combines prepend with dynamic method definition to create context-aware functionality.
module ConditionalLogging
def self.prepended(base)
base.instance_methods(false).each do |method_name|
next if method_name.to_s.start_with?('_')
original_method = base.instance_method(method_name)
base.define_method(method_name) do |*args, &block|
if ENV['DEBUG']
puts "Calling #{base}##{method_name} with #{args.inspect}"
result = original_method.bind(self).call(*args, &block)
puts "Result: #{result.inspect}"
result
else
original_method.bind(self).call(*args, &block)
end
end
end
end
end
class DataProcessor
def process(data)
data.upcase
end
def transform(data)
data.reverse
end
end
class DataProcessor
prepend ConditionalLogging
end
Module composition through prepend enables building complex functionality from smaller, focused modules. Each module can assume the presence of methods from modules higher in the chain, creating layered architectures.
module Cacheable
def get(key)
@cache ||= {}
@cache[key] ||= super
end
end
module Validatable
def get(key)
raise ArgumentError, "Invalid key" if key.nil? || key.empty?
super
end
end
module Loggable
def get(key)
puts "Fetching: #{key}"
result = super
puts "Found: #{result}"
result
end
end
class DataStore
prepend Cacheable, Validatable, Loggable
def get(key)
"value_for_#{key}"
end
end
store = DataStore.new
store.get("user_123")
# => Fetching: user_123
# => Found: value_for_user_123
# => "value_for_user_123"
Prepend hooks enable modules to execute code when they're prepended to classes. The prepended
callback method receives the target class as an argument, allowing modules to modify the class or set up additional functionality.
module Trackable
def self.prepended(base)
base.extend(ClassMethods)
base.class_eval do
attr_reader :tracked_changes
end
end
module ClassMethods
def tracked_attributes(*attrs)
@tracked_attrs = attrs
attrs.each do |attr|
define_method("#{attr}=") do |value|
@tracked_changes ||= {}
old_value = instance_variable_get("@#{attr}")
@tracked_changes[attr] = [old_value, value] if old_value != value
instance_variable_set("@#{attr}", value)
end
end
end
end
def initialize(*args)
@tracked_changes = {}
super
end
end
class User
prepend Trackable
attr_reader :name, :email
tracked_attributes :name, :email
def initialize(name, email)
@name = name
@email = email
super
end
end
user = User.new("John", "john@example.com")
user.name = "Jane"
user.tracked_changes
# => {:name=>["John", "Jane"]}
Common Pitfalls
Method resolution conflicts arise when multiple prepended modules define the same method. The last module prepended takes precedence, but this can create unexpected behavior when modules are prepended in different orders across a codebase.
module A
def process
"A processing"
end
end
module B
def process
"B processing"
end
end
class Worker
prepend A, B # B takes precedence
end
class Manager
prepend B, A # A takes precedence
end
Worker.new.process # => "B processing"
Manager.new.process # => "A processing"
The super
keyword behavior differs significantly between prepend and include scenarios. With prepend, super
calls the original class method, while with include, super
would call the superclass method. This difference can break existing code when switching between inclusion strategies.
module Wrapper
def calculate(x)
puts "Before calculation"
result = super
puts "After calculation"
result
end
end
class Calculator
def calculate(x)
x * 2
end
end
# Different behaviors:
class PrependCalculator < Calculator
prepend Wrapper # super calls Calculator#calculate
end
class IncludeCalculator < Calculator
include Wrapper # super would call parent class method
end
Infinite recursion occurs when prepended modules call methods that don't exist in the class or its ancestors. Without a terminating super
call, Ruby continues searching the ancestor chain indefinitely.
module Problematic
def nonexistent_method
puts "Calling super"
super # NoMethodError - no method to call
end
end
class Empty
prepend Problematic
end
# Empty.new.nonexistent_method # => NoMethodError
Module state isolation problems emerge when modules store state in instance variables that might conflict with the class or other modules. Prepended modules share the same object instance as the class, creating potential variable name collisions.
module Counter
def increment
@count = (@count || 0) + 1
end
def count
@count || 0
end
end
class Statistics
prepend Counter
def initialize
@count = "string value" # Conflicts with Counter's @count
end
end
stats = Statistics.new
stats.increment
# TypeError: String can't be coerced into Integer
Class versus instance method confusion occurs because prepend only affects instance methods by default. Class methods require explicit prepending to the singleton class, which developers often forget.
module Logger
def self.log(message)
puts "Module: #{message}"
end
def log(message)
puts "Instance: #{message}"
end
end
class Service
prepend Logger # Only prepends instance methods
def self.log(message)
puts "Class: #{message}"
end
def log(message)
puts "Service: #{message}"
end
end
Service.log("test") # => "Class: test" (module method not prepended)
Service.new.log("test") # => "Instance: test" (prepended successfully)
Hook method dependencies can create subtle bugs when modules expect certain hook methods to be called in specific orders. The prepended
hook executes immediately when prepend is called, not when the class is fully defined.
module EarlyBird
def self.prepended(base)
puts "Prepended to #{base}"
base.some_method # May not exist yet
end
end
class Target
prepend EarlyBird # Hook runs before some_method is defined
def some_method
"Available now"
end
end
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Module.prepend(*modules) |
*modules (Module) |
self |
Inserts modules at the beginning of method lookup chain |
Module.prepended(base) |
base (Class/Module) |
No return value | Callback executed when module is prepended |
Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Class.ancestors |
None | Array<Module> |
Returns ancestor chain including prepended modules |
Module.prepend_features(mod) |
mod (Module) |
mod |
Low-level prepend implementation method |
Method Resolution Order
Prepended modules appear at the front of the ancestor chain:
class Base; end
module M1; end
module M2; end
class Example < Base
include M1
prepend M2
end
Example.ancestors
# => [M2, Example, M1, Base, Object, Kernel, BasicObject]
Hook Methods Execution Order
Hook | Trigger | Execution Context |
---|---|---|
prepended |
When prepend is called |
Module's singleton class |
included |
When include is called |
Module's singleton class |
extended |
When extend is called |
Module's singleton class |
Common Patterns
Method Decoration:
module Decorator
def method_name(*args)
# pre-processing
result = super
# post-processing
result
end
end
Conditional Override:
module ConditionalOverride
def method_name(*args)
return custom_behavior if condition?
super
end
end
State Injection:
module StateInjection
def initialize(*args)
super
@injected_state = initial_value
end
end
Error Types
Error | Cause | Solution |
---|---|---|
NoMethodError |
Calling super with no method to call |
Check ancestor chain or provide default implementation |
ArgumentError |
Wrong number of arguments to super |
Match parameter signatures between module and class methods |
TypeError |
Instance variable type conflicts | Use namespaced instance variable names |
SystemStackError |
Infinite recursion in super calls |
Ensure terminating condition exists in ancestor chain |
Performance Characteristics
- Method lookup overhead increases with prepended module count
- Each prepended module adds one step to method resolution
- Anonymous singleton class creation has minimal memory overhead
super
calls maintain same performance as regular method calls