CrackedRuby logo

CrackedRuby

Dynamic Module Creation

A comprehensive guide to creating, modifying, and composing modules at runtime using Ruby's metaprogramming capabilities.

Metaprogramming Code Generation
5.8.2

Overview

Dynamic module creation in Ruby enables developers to construct and modify modules programmatically during runtime. Ruby provides several mechanisms for this functionality, including Module.new, define_method, const_set, and various reflection methods. This approach differs from static module definition by allowing the structure, methods, and behavior of modules to be determined by runtime conditions, configuration data, or external inputs.

The primary classes involved in dynamic module creation are Module and Class, along with supporting methods from Object and Kernel. The Module class serves as the foundation, providing methods for creating anonymous modules, defining methods dynamically, and managing constants. The Class class inherits from Module and adds constructor behavior, making it suitable for creating both modules and classes programmatically.

Ruby's module system operates through a method lookup chain that includes modules mixed into classes via include, prepend, and extend. Dynamic module creation leverages this system by constructing modules that integrate seamlessly with existing code. The interpreter handles method resolution, constant lookup, and inheritance relationships automatically, regardless of whether modules are defined statically or created dynamically.

# Basic anonymous module creation
dynamic_module = Module.new do
  def greet(name)
    "Hello, #{name}!"
  end
end

class User
  include dynamic_module
end

user = User.new
user.greet("Alice")  # => "Hello, Alice!"

Common use cases include plugin architectures, configuration-driven behavior, code generation from external schemas, and runtime adaptation to changing requirements. Dynamic modules excel in scenarios where the exact structure of code cannot be predetermined, such as API clients generated from service descriptions, domain-specific languages, or frameworks that need to adapt to user-defined schemas.

# Configuration-driven module creation
def create_validator_module(rules)
  Module.new do
    rules.each do |field, validations|
      define_method("validate_#{field}") do |value|
        validations.all? { |validation| validation.call(value) }
      end
    end
  end
end

validation_rules = {
  email: [proc { |v| v.include?("@") }, proc { |v| v.length > 5 }],
  age: [proc { |v| v.is_a?(Integer) }, proc { |v| v > 0 }]
}

validator = create_validator_module(validation_rules)

The module creation process involves several steps: instantiating the module object, defining its interface through methods and constants, and integrating it with existing classes or other modules. Ruby handles memory management and garbage collection for dynamically created modules, cleaning up unused modules when they become unreachable.

Basic Usage

The Module.new constructor creates anonymous modules that can be assigned to constants or used directly. When called with a block, the block executes in the context of the new module, allowing method definitions and constant assignments within the module's scope. This pattern forms the foundation of most dynamic module creation scenarios.

# Anonymous module with block initialization
StringUtils = Module.new do
  def self.reverse_words(text)
    text.split.reverse.join(" ")
  end
  
  def titleize
    self.split.map(&:capitalize).join(" ")
  end
end

String.include(StringUtils)
"hello world".titleize  # => "Hello World"
StringUtils.reverse_words("hello world")  # => "world hello"

The define_method method creates instance methods dynamically within modules. Unlike def statements, define_method accepts the method name as a parameter, enabling method creation based on runtime data. The method body can be provided as a block or a Proc object, and the method gains access to variables from the surrounding lexical scope through closure behavior.

# Dynamic method definition from configuration
def create_accessor_module(attributes)
  Module.new do
    attributes.each do |attr_name, default_value|
      define_method(attr_name) do
        instance_variable_get("@#{attr_name}") || default_value
      end
      
      define_method("#{attr_name}=") do |value|
        instance_variable_set("@#{attr_name}", value)
      end
    end
  end
end

UserAttributes = create_accessor_module({
  name: "Anonymous",
  email: nil,
  active: true
})

class User
  include UserAttributes
end

user = User.new
user.name          # => "Anonymous"
user.email = "test@example.com"
user.email         # => "test@example.com"

Constants within dynamic modules are managed using const_set and const_get methods. These methods operate on the module's constant table, allowing runtime assignment and retrieval of constant values. Constants defined this way follow the same scoping and lookup rules as statically defined constants, including inheritance and nested constant resolution.

# Dynamic constant definition
APIModule = Module.new do
  const_set(:VERSION, "2.1.0")
  const_set(:BASE_URL, "https://api.example.com")
  
  endpoints = ["users", "posts", "comments"]
  endpoints.each do |endpoint|
    const_set(endpoint.upcase, "/#{endpoint}")
  end
end

APIModule::VERSION    # => "2.1.0"
APIModule::USERS      # => "/users"
APIModule::POSTS      # => "/posts"

Module inclusion and extension work identically with dynamic modules as with static modules. The include method adds the module's instance methods to the including class, while extend adds them as singleton methods. The prepend method, introduced in Ruby 2.0, adds the module's methods earlier in the method lookup chain than the class's own methods.

# Dynamic module with different inclusion strategies
LoggingModule = Module.new do
  def log_action(action)
    puts "[#{Time.now}] #{self.class.name}: #{action}"
  end
  
  def self.included(base)
    puts "LoggingModule included in #{base.name}"
  end
  
  def self.extended(base)
    puts "LoggingModule extended by #{base.name}"
  end
end

class Service
  include LoggingModule
end

class Utility
  extend LoggingModule
end

Service.new.log_action("processing")  # Instance method
Utility.log_action("calculating")     # Class method

Advanced Usage

Module builders represent a sophisticated pattern for creating families of related modules with shared structure but varying behavior. These builders encapsulate the module creation logic within classes or modules, providing a clean interface for generating modules based on complex specifications or configurations.

class ServiceModuleBuilder
  def initialize(service_name, base_url)
    @service_name = service_name
    @base_url = base_url
    @endpoints = {}
    @middleware = []
  end
  
  def add_endpoint(name, path, http_method = :get)
    @endpoints[name] = { path: path, method: http_method }
    self
  end
  
  def add_middleware(middleware_proc)
    @middleware << middleware_proc
    self
  end
  
  def build
    service_name = @service_name
    base_url = @base_url
    endpoints = @endpoints
    middleware = @middleware
    
    Module.new do
      const_set(:SERVICE_NAME, service_name)
      const_set(:BASE_URL, base_url)
      
      endpoints.each do |name, config|
        define_method(name) do |params = {}|
          url = "#{base_url}#{config[:path]}"
          request_data = { method: config[:method], url: url, params: params }
          
          # Apply middleware transformations
          middleware.each do |mw|
            request_data = mw.call(request_data)
          end
          
          # Simulated HTTP request
          "#{config[:method].upcase} #{request_data[:url]} with #{request_data[:params]}"
        end
      end
      
      define_method(:service_info) do
        "Service: #{service_name}, Base URL: #{base_url}"
      end
    end
  end
end

# Usage of the builder
api_client = ServiceModuleBuilder
  .new("UserService", "https://api.users.com")
  .add_endpoint(:get_user, "/users/:id")
  .add_endpoint(:create_user, "/users", :post)
  .add_middleware(proc { |req| req[:headers] = { "Accept" => "application/json" }; req })
  .build

class UserManager
  include api_client
end

manager = UserManager.new
manager.get_user(id: 123)  # => "GET https://api.users.com/users/:id with {:id=>123}"

Nested module creation involves constructing modules within other modules, creating hierarchical namespaces dynamically. This pattern requires careful handling of constant scoping and method visibility, as nested modules inherit certain behaviors from their parent modules.

def create_nested_api_modules(service_configs)
  root_module = Module.new
  
  service_configs.each do |service_name, config|
    service_module = Module.new do
      const_set(:VERSION, config[:version])
      
      config[:resources].each do |resource_name, methods|
        resource_module = Module.new do
          methods.each do |method_name, implementation|
            define_method(method_name, &implementation)
          end
        end
        
        const_set(resource_name.to_s.capitalize, resource_module)
      end
    end
    
    root_module.const_set(service_name.to_s.capitalize, service_module)
  end
  
  root_module
end

config = {
  billing: {
    version: "1.0",
    resources: {
      invoices: {
        create: proc { |data| "Creating invoice with #{data}" },
        list: proc { "Listing all invoices" }
      },
      payments: {
        process: proc { |amount| "Processing payment of #{amount}" }
      }
    }
  },
  inventory: {
    version: "2.0",
    resources: {
      items: {
        stock_check: proc { |id| "Checking stock for item #{id}" }
      }
    }
  }
}

APIModules = create_nested_api_modules(config)

# Access nested functionality
billing_invoices = APIModules::Billing::Invoices
inventory_items = APIModules::Inventory::Items

class OrderProcessor
  include billing_invoices
  include inventory_items
end

processor = OrderProcessor.new
processor.create(data: { customer_id: 1, amount: 100 })  # => "Creating invoice with {...}"
processor.stock_check(42)  # => "Checking stock for item 42"

Method delegation and forwarding within dynamic modules enables the creation of proxy objects and adapter patterns. These patterns involve creating methods that forward calls to other objects or modules, often with transformation or validation logic applied during the forwarding process.

def create_delegating_module(target_methods)
  Module.new do
    target_methods.each do |local_name, target_config|
      target_object = target_config[:to]
      target_method = target_config[:method] || local_name
      transform_proc = target_config[:transform]
      
      define_method(local_name) do |*args, **kwargs|
        result = target_object.public_send(target_method, *args, **kwargs)
        transform_proc ? transform_proc.call(result) : result
      end
    end
  end
end

# External service objects
email_service = Object.new
def email_service.send_email(to, subject, body)
  "Email sent to #{to}: #{subject}"
end

sms_service = Object.new  
def sms_service.send_sms(phone, message)
  "SMS sent to #{phone}: #{message}"
end

# Create delegating notification module
NotificationModule = create_delegating_module({
  send_email: {
    to: email_service,
    method: :send_email
  },
  send_notification: {
    to: sms_service,
    method: :send_sms,
    transform: proc { |result| "[NOTIFICATION] #{result}" }
  }
})

class NotificationCenter
  include NotificationModule
end

center = NotificationCenter.new
center.send_email("user@example.com", "Welcome", "Hello!")  
# => "Email sent to user@example.com: Welcome"
center.send_notification("555-0123", "Alert!")  
# => "[NOTIFICATION] SMS sent to 555-0123: Alert!"

Common Pitfalls

Constant assignment timing creates subtle bugs when constants are set within dynamic modules after the module has been included in classes. Ruby resolves constants at the time of first access, not at include time, which can lead to NameError exceptions if constants are assigned after inclusion but before first use.

# Problematic constant assignment timing
ProblematicModule = Module.new

class User
  include ProblematicModule  # Module included before constants are set
end

# Constants assigned after inclusion
ProblematicModule.const_set(:DEFAULT_ROLE, "guest")
ProblematicModule.const_set(:VALID_STATUSES, ["active", "inactive"])

# This works fine
User.new.class::DEFAULT_ROLE  # => "guest"

# But this fails in method definitions within the module
ProblematicModule.module_eval do
  def get_default_role
    DEFAULT_ROLE  # NameError: uninitialized constant unless carefully handled
  end
end

# Correct approach: set constants before inclusion
CorrectModule = Module.new do
  const_set(:DEFAULT_ROLE, "guest")
  const_set(:VALID_STATUSES, ["active", "inactive"])
  
  def get_default_role
    DEFAULT_ROLE  # Works correctly
  end
end

Scope capture in closures can cause memory leaks and unexpected behavior when large objects are inadvertently captured by method definitions within dynamic modules. The closure created by blocks passed to define_method retains references to all variables in scope, potentially preventing garbage collection of large data structures.

# Memory leak through closure capture
def create_leaky_module(large_dataset)
  processed_data = large_dataset.map { |item| expensive_processing(item) }
  summary = generate_summary(processed_data)
  
  Module.new do
    # This closure captures both large_dataset AND processed_data
    define_method(:get_summary) do
      summary  # Only needs summary, but closure captures everything
    end
    
    # Better approach: extract needed values first
    summary_value = summary
    define_method(:get_summary_safe) do
      summary_value  # Only captures the specific value needed
    end
  end
end

# Even better: use module_eval with string to avoid closures entirely
def create_safe_module(large_dataset)
  summary = generate_summary(large_dataset)
  
  Module.new.tap do |mod|
    mod.const_set(:SUMMARY, summary)
    mod.module_eval <<-RUBY
      def get_summary
        SUMMARY
      end
    RUBY
  end
end

Method visibility inheritance behaves unexpectedly with dynamic modules when methods are defined as private or protected. The visibility setting applies to methods defined after the visibility declaration, but dynamic method definition timing can create inconsistent visibility across methods within the same module.

# Visibility confusion with dynamic methods
VisibilityModule = Module.new do
  def public_method
    "This is public"
  end
  
  private  # This affects subsequent method definitions
  
  def private_static_method
    "This is private"  
  end
  
  # Methods defined dynamically after 'private' are still private
  define_method(:private_dynamic_method) do
    "This is also private"
  end
  
  public  # Reset visibility
  
  define_method(:public_dynamic_method) do
    "This is public again"
  end
end

class TestClass
  include VisibilityModule
end

test = TestClass.new
test.public_method              # Works
test.public_dynamic_method      # Works
# test.private_static_method    # NoMethodError: private method
# test.private_dynamic_method   # NoMethodError: private method

# Explicit visibility control for dynamic methods
ExplicitVisibilityModule = Module.new do
  define_method(:helper_method) { "Helper logic" }
  private :helper_method  # Explicitly make private after definition
  
  define_method(:public_interface) do
    "Public: #{helper_method}"  # Can call private method internally
  end
end

Module evaluation context confusion arises when module_eval, class_eval, and instance_eval are used interchangeably without understanding their different execution contexts. Each method evaluates code in a different binding, affecting variable access, method definitions, and constant lookup.

# Context confusion demonstration
outer_variable = "accessible"

TestModule = Module.new

# module_eval executes in the module's context
TestModule.module_eval do
  puts outer_variable  # Accessible through closure
  
  def module_eval_method
    "Defined as instance method"
  end
  
  define_singleton_method(:module_singleton) do
    "Defined as module singleton method"
  end
end

# instance_eval executes in the context of the module object itself
TestModule.instance_eval do
  puts outer_variable  # Still accessible through closure
  
  def instance_eval_method
    "Defined as singleton method on the module"
  end
end

# The methods end up in different places:
class TestClass
  include TestModule
end

TestClass.new.module_eval_method  # Instance method from module_eval
TestModule.module_singleton       # Module singleton method
TestModule.instance_eval_method   # Module singleton method

# class_eval is an alias for module_eval but suggests class context
# Use module_eval for modules, class_eval for classes for clarity

Include order dependencies become critical when multiple dynamic modules define methods with the same name or when modules depend on methods defined in other modules. Ruby's method lookup chain follows the reverse order of inclusion, and dynamic module creation can make these dependencies non-obvious.

# Include order dependency issues
BaseModule = Module.new do
  def process_data(data)
    "Base processing: #{data}"
  end
end

# This module depends on process_data being available
EnhancingModule = Module.new do
  def enhanced_process(data)
    "Enhanced: #{process_data(data)}"  # Expects process_data to exist
  end
end

# Wrong order - EnhancingModule included before BaseModule
class ProblematicProcessor
  include EnhancingModule  # This module needs process_data
  include BaseModule       # This module provides process_data
end

# This works because method lookup finds process_data in BaseModule
processor = ProblematicProcessor.new
processor.enhanced_process("test")  # => "Enhanced: Base processing: test"

# But consider this case where dependency is not met:
StandaloneEnhancing = Module.new do
  def standalone_enhanced(data)
    "Standalone: #{process_data(data)}"  # Will fail if no process_data
  end
end

class FailingProcessor
  include StandaloneEnhancing  # No process_data available
end

# This fails at runtime
# FailingProcessor.new.standalone_enhanced("test")  # NoMethodError

Reference

Module Creation Methods

Method Parameters Returns Description
Module.new &block (optional) Module Creates anonymous module, evaluates block in module context if provided
Module.new { ... } Block Module Creates module and executes block in module's context
Class.new superclass = Object, &block Class Creates anonymous class with optional superclass and initialization block
Class.new(SuperClass) { ... } Superclass, block Class Creates class inheriting from superclass with block evaluation

Dynamic Method Definition

Method Parameters Returns Description
define_method(name) Symbol/String, block Symbol Defines instance method with given name and block body
define_method(name, method) Symbol/String, Method/Proc Symbol Defines instance method using existing method or proc
define_singleton_method(name) Symbol/String, block Symbol Defines singleton method on the module/class object
alias_method(new_name, old_name) Symbol/String, Symbol/String Module Creates alias for existing method
undef_method(name) Symbol/String Module Removes method definition from module
remove_method(name) Symbol/String Module Removes method from current class/module only

Constant Management

Method Parameters Returns Description
const_set(name, value) Symbol/String, Object Object Assigns constant within module namespace
const_get(name) Symbol/String, Boolean (inherit) Object Retrieves constant value, optionally searching ancestors
const_defined?(name) Symbol/String, Boolean (inherit) Boolean Checks if constant is defined in module or ancestors
constants Boolean (inherit) Array Returns array of constant names defined in module
const_missing(name) Symbol Object Hook called when constant lookup fails, can be overridden

Module Inclusion and Extension

Method Parameters Returns Description
include(*modules) Module(s) self Adds modules as ancestors, methods become instance methods
prepend(*modules) Module(s) self Adds modules early in ancestor chain, before class methods
extend(*modules) Module(s) self Adds module methods as singleton methods on receiver
included_modules None Array Returns modules included in receiver's ancestor chain
ancestors None Array Returns complete ancestor chain including classes and modules

Code Evaluation Methods

Method Parameters Returns Description
module_eval(string) String Object Evaluates string in module context
module_eval { block } Block Object Evaluates block in module context
class_eval(string) String Object Alias for module_eval, preferred for classes
instance_eval(string) String Object Evaluates string in context of module object
instance_eval { block } Block Object Evaluates block in context of module object

Callback Hooks

Hook Method When Called Parameters Description
included(base) When module is included Including class/module Called after module inclusion
extended(base) When module is extended Extending object Called after module extension
prepended(base) When module is prepended Prepending class/module Called after module prepending
const_missing(name) When constant not found Constant name Opportunity to define missing constant
method_missing(name, *args) When method not found Method name, arguments Handle undefined method calls

Introspection and Reflection

Method Parameters Returns Description
instance_methods Boolean (include_super) Array Returns instance method names
private_instance_methods Boolean (include_super) Array Returns private instance method names
singleton_methods Boolean (all) Array Returns singleton method names
method_defined?(name) Symbol/String Boolean Checks if instance method is defined
private_method_defined?(name) Symbol/String Boolean Checks if private method is defined
respond_to?(name) Symbol/String, Boolean (include_private) Boolean Checks if object responds to method

Visibility Control

Method Parameters Returns Description
private(*names) Symbol/String(s) Module Makes methods private
protected(*names) Symbol/String(s) Module Makes methods protected
public(*names) Symbol/String(s) Module Makes methods public
private_constant(*names) Symbol/String(s) Module Makes constants private
public_constant(*names) Symbol/String(s) Module Makes constants public

Common Error Classes

Error Class Common Causes Description
NameError Undefined constant, undefined method Name resolution failures
NoMethodError Method not found, private method called Method call failures
ArgumentError Wrong argument count, invalid arguments Argument validation failures
TypeError Wrong object type, conversion failures Type-related errors
RuntimeError General runtime issues Default error for raise