CrackedRuby logo

CrackedRuby

prepend Method

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