CrackedRuby logo

CrackedRuby

Method Parameters

Method parameters in Ruby define how methods accept and process input, including positional, keyword, splat, and block parameters.

Metaprogramming Method Objects
5.6.4

Overview

Ruby method parameters provide flexible mechanisms for passing data to methods through multiple parameter types. The language supports positional parameters, keyword parameters, splat parameters for variable arguments, double splat parameters for keyword argument capture, and block parameters for code execution.

The parameter system operates through method signatures that define parameter names, types, default values, and constraints. Ruby evaluates parameters left-to-right during method calls, with specific precedence rules governing parameter matching and argument assignment.

def process_data(required, optional = "default", *args, keyword:, optional_kw: nil, **kwargs, &block)
  puts "Required: #{required}"
  puts "Optional: #{optional}" 
  puts "Splat args: #{args}"
  puts "Required keyword: #{keyword}"
  puts "Optional keyword: #{optional_kw}"
  puts "Keyword hash: #{kwargs}"
  block.call if block_given?
end

process_data("value", "custom", "extra1", "extra2", keyword: "req", other: "data") { puts "Block executed" }
# Required: value
# Optional: custom
# Splat args: ["extra1", "extra2"]
# Required keyword: req
# Optional keyword: 
# Keyword hash: {:other=>"data"}
# Block executed

Method parameters integrate with Ruby's dynamic nature through argument forwarding, parameter introspection via Method objects, and runtime parameter validation. The system handles type coercion, parameter binding, and scope management during method invocation.

class DataProcessor
  def initialize(strategy: :basic, **options)
    @strategy = strategy
    @options = options
  end

  def process(*items, transform: nil, &block)
    items.map do |item|
      result = apply_strategy(item)
      result = transform.call(result) if transform
      result = block.call(result) if block_given?
      result
    end
  end

  private

  def apply_strategy(item)
    case @strategy
    when :basic then item.to_s.upcase
    when :reverse then item.to_s.reverse
    else item
    end
  end
end

processor = DataProcessor.new(strategy: :reverse, debug: true)
results = processor.process("hello", "world", transform: ->(x) { x + "!" }) { |x| "[#{x}]" }
# => ["[!olleh]", "[!dlrow]"]

Basic Usage

Ruby methods accept parameters through several distinct mechanisms, each serving different use cases and providing varying degrees of flexibility.

Positional Parameters

Positional parameters receive arguments based on their position in the method signature. Required positional parameters must receive arguments, while optional positional parameters use default values when arguments are omitted.

def create_user(name, email, role = "user", active = true)
  {
    name: name,
    email: email, 
    role: role,
    active: active,
    created_at: Time.now
  }
end

user1 = create_user("Alice", "alice@example.com")
# => {:name=>"Alice", :email=>"alice@example.com", :role=>"user", :active=>true, :created_at=>...}

user2 = create_user("Bob", "bob@example.com", "admin", false)
# => {:name=>"Bob", :email=>"bob@example.com", :role=>"admin", :active=>false, :created_at=>...}

Default parameter values are evaluated at method call time, not method definition time. This enables dynamic default values but requires caution with mutable objects.

def log_message(text, timestamp = Time.now, tags = [])
  tags << "logged" # Modifies the default array
  puts "#{timestamp}: #{text} [#{tags.join(', ')}]"
  tags
end

log_message("First call") # => ["logged"]
log_message("Second call") # => ["logged", "logged"] - Same array object
log_message("Third call", Time.now, ["custom"]) # => ["custom", "logged"]

Keyword Parameters

Keyword parameters provide named argument passing, improving method call clarity and enabling flexible parameter ordering. Ruby distinguishes between required and optional keyword parameters.

def configure_database(host:, port: 5432, username:, password:, ssl: false, **options)
  config = {
    host: host,
    port: port,
    username: username,
    password: password,
    ssl: ssl
  }.merge(options)
  
  puts "Connecting to #{config[:host]}:#{config[:port]}"
  puts "SSL: #{config[:ssl] ? 'enabled' : 'disabled'}"
  puts "Additional options: #{options}" unless options.empty?
  config
end

# Arguments can be provided in any order
db_config = configure_database(
  username: "admin",
  host: "localhost", 
  password: "secret",
  timeout: 30,
  pool_size: 10
)

Keyword parameters support complex validation patterns and conditional defaults through method implementation.

def process_payment(amount:, currency: "USD", method: :credit_card, **details)
  raise ArgumentError, "Amount must be positive" if amount <= 0
  
  case method
  when :credit_card
    required_fields = [:card_number, :expiry, :cvv]
    missing = required_fields - details.keys
    raise ArgumentError, "Missing credit card fields: #{missing}" unless missing.empty?
  when :bank_transfer
    required_fields = [:account_number, :routing_number]
    missing = required_fields - details.keys
    raise ArgumentError, "Missing bank transfer fields: #{missing}" unless missing.empty?
  end

  {
    amount: amount,
    currency: currency,
    method: method,
    details: details,
    processed_at: Time.now
  }
end

# Valid credit card payment
payment1 = process_payment(
  amount: 100.00,
  method: :credit_card,
  card_number: "1234-5678-9012-3456",
  expiry: "12/25",
  cvv: "123"
)

Variable Arguments

Splat parameters capture variable numbers of positional arguments into arrays, while double splat parameters capture variable keyword arguments into hashes.

def analyze_data(*datasets, format: :json, **options)
  results = {}
  
  datasets.each_with_index do |data, index|
    key = "dataset_#{index + 1}"
    results[key] = {
      size: data.size,
      type: data.class.name,
      format: format
    }.merge(options)
  end
  
  results
end

analysis = analyze_data(
  [1, 2, 3, 4, 5],
  {"a" => 1, "b" => 2},
  "sample string",
  format: :xml,
  validator: "strict",
  encoding: "utf-8"
)
# => {
#   :dataset_1 => {:size=>5, :type=>"Array", :format=>:xml, :validator=>"strict", :encoding=>"utf-8"},
#   :dataset_2 => {:size=>2, :type=>"Hash", :format=>:xml, :validator=>"strict", :encoding=>"utf-8"}, 
#   :dataset_3 => {:size=>13, :type=>"String", :format=>:xml, :validator=>"strict", :encoding=>"utf-8"}
# }

Advanced Usage

Ruby method parameters support sophisticated patterns through parameter forwarding, metaprogramming integration, and dynamic parameter manipulation.

Parameter Forwarding

Ruby provides argument forwarding operators that delegate parameters to other methods while preserving parameter types and structure. The ... operator forwards all arguments, while individual forwarding preserves specific parameter categories.

class APIWrapper
  def initialize(base_url, **default_options)
    @base_url = base_url
    @default_options = default_options
  end

  def get(path, **options, &block)
    request(:get, path, **@default_options.merge(options), &block)
  end

  def post(path, body = nil, **options, &block)
    request(:post, path, body: body, **@default_options.merge(options), &block)
  end

  # Forward all arguments to underlying HTTP method
  def request(method, path, ...)
    http_request(method, "#{@base_url}#{path}", ...)
  end

  private

  def http_request(method, url, body: nil, headers: {}, timeout: 30, &block)
    puts "#{method.upcase} #{url}"
    puts "Headers: #{headers}" unless headers.empty?
    puts "Body: #{body}" if body
    puts "Timeout: #{timeout}"
    result = { status: 200, data: "response" }
    block.call(result) if block_given?
    result
  end
end

api = APIWrapper.new("https://api.example.com", headers: { "Authorization" => "Bearer token" })

response = api.get("/users", timeout: 60) do |result|
  puts "Processing response: #{result[:status]}"
end

Metaprogramming with Parameters

Method parameters integrate with Ruby's metaprogramming capabilities through Method objects, parameter introspection, and dynamic method definition.

class DynamicProcessor
  def self.create_processor(name, *required_params, **default_options, &validator)
    define_method("process_#{name}") do |*args, **options, &block|
      # Validate required parameters
      if args.length < required_params.length
        missing = required_params[args.length..-1]
        raise ArgumentError, "Missing required parameters: #{missing}"
      end

      # Build parameter hash
      params = {}
      required_params.each_with_index { |param, i| params[param] = args[i] }
      
      # Merge options with defaults
      final_options = default_options.merge(options)
      
      # Apply validation if provided
      if validator
        validation_result = validator.call(params, final_options)
        raise ArgumentError, "Validation failed: #{validation_result}" if validation_result.is_a?(String)
      end

      # Execute processing logic
      result = {
        processor: name,
        parameters: params,
        options: final_options,
        processed_at: Time.now
      }

      block.call(result) if block_given?
      result
    end
  end

  # Create processors with different parameter configurations
  create_processor(:user_data, :name, :email, format: :json, validate: true) do |params, options|
    return "Invalid email format" unless params[:email].include?("@")
    return "Name too short" if params[:name].length < 2
    true
  end

  create_processor(:file_data, :path, :content, encoding: "utf-8", compress: false) do |params, options|
    return "File path cannot be empty" if params[:path].empty?
    return "Content must be string" unless params[:content].is_a?(String)
    true
  end
end

processor = DynamicProcessor.new

user_result = processor.process_user_data("Alice Smith", "alice@example.com", validate: false) do |result|
  puts "User processed: #{result[:parameters][:name]}"
end

Complex Parameter Patterns

Advanced parameter patterns combine multiple parameter types with validation, transformation, and conditional logic.

class ConfigurationBuilder
  def initialize
    @configurations = {}
  end

  def add_config(name, *layers, required: [], optional: {}, transforms: {}, &validator)
    # Merge configuration layers
    config = layers.reduce({}) do |merged, layer|
      case layer
      when Hash then merged.merge(layer)
      when String then merged.merge(load_config_file(layer))
      else raise ArgumentError, "Invalid configuration layer: #{layer.class}"
      end
    end

    # Apply optional defaults
    config = optional.merge(config)

    # Validate required keys
    missing_required = required - config.keys
    raise ArgumentError, "Missing required configuration: #{missing_required}" unless missing_required.empty?

    # Apply transformations
    transforms.each do |key, transform|
      config[key] = transform.call(config[key]) if config.key?(key)
    end

    # Custom validation
    if validator
      validation_errors = validator.call(config)
      raise ArgumentError, "Configuration validation failed: #{validation_errors}" if validation_errors
    end

    @configurations[name] = config
  end

  def get_config(name, **overrides)
    base_config = @configurations[name] or raise ArgumentError, "Unknown configuration: #{name}"
    base_config.merge(overrides)
  end

  private

  def load_config_file(path)
    # Simulate file loading
    case File.extname(path)
    when '.json' then { loaded_from: path, format: 'json' }
    when '.yaml' then { loaded_from: path, format: 'yaml' }
    else {}
    end
  end
end

builder = ConfigurationBuilder.new

builder.add_config(
  :database,
  { host: "localhost", port: 5432 },
  "/etc/app/database.json",
  { username: "admin", password: "secret" },
  required: [:host, :username, :password],
  optional: { timeout: 30, pool_size: 5, ssl: false },
  transforms: {
    host: ->(h) { h.downcase.strip },
    port: ->(p) { p.to_i },
    ssl: ->(s) { !!s }
  }
) do |config|
  errors = []
  errors << "Port must be positive integer" unless config[:port].positive?
  errors << "Username cannot be empty" if config[:username].empty?
  errors.empty? ? nil : errors
end

db_config = builder.get_config(:database, pool_size: 10, ssl: true)

Parameter Object Patterns

Complex parameter handling benefits from parameter object patterns that encapsulate parameter validation, transformation, and access.

class SearchParameters
  VALID_ORDERS = %w[asc desc].freeze
  VALID_FIELDS = %w[name email created_at updated_at].freeze

  attr_reader :query, :page, :per_page, :order, :order_by, :filters

  def initialize(query: nil, page: 1, per_page: 10, order: "asc", order_by: "created_at", **filters)
    @query = normalize_query(query)
    @page = validate_page(page)
    @per_page = validate_per_page(per_page)
    @order = validate_order(order)
    @order_by = validate_order_by(order_by)
    @filters = normalize_filters(filters)
  end

  def offset
    (page - 1) * per_page
  end

  def to_sql_conditions
    conditions = []
    parameters = []

    if query
      conditions << "name ILIKE ? OR email ILIKE ?"
      parameters.concat(["%#{query}%", "%#{query}%"])
    end

    filters.each do |field, value|
      next unless VALID_FIELDS.include?(field.to_s)
      conditions << "#{field} = ?"
      parameters << value
    end

    [conditions.join(" AND "), parameters]
  end

  def to_h
    {
      query: query,
      page: page,
      per_page: per_page,
      order: order,
      order_by: order_by,
      offset: offset,
      filters: filters
    }
  end

  private

  def normalize_query(query)
    return nil if query.nil? || query.to_s.strip.empty?
    query.to_s.strip
  end

  def validate_page(page)
    page_int = Integer(page)
    raise ArgumentError, "Page must be positive" unless page_int.positive?
    page_int
  rescue TypeError, ArgumentError => e
    raise ArgumentError, "Invalid page parameter: #{e.message}"
  end

  def validate_per_page(per_page)
    per_page_int = Integer(per_page)
    raise ArgumentError, "Per page must be between 1 and 100" unless (1..100).cover?(per_page_int)
    per_page_int
  rescue TypeError, ArgumentError => e
    raise ArgumentError, "Invalid per_page parameter: #{e.message}"
  end

  def validate_order(order)
    order_str = order.to_s.downcase
    raise ArgumentError, "Order must be 'asc' or 'desc'" unless VALID_ORDERS.include?(order_str)
    order_str
  end

  def validate_order_by(order_by)
    order_by_str = order_by.to_s.downcase
    raise ArgumentError, "Invalid order_by field" unless VALID_FIELDS.include?(order_by_str)
    order_by_str
  end

  def normalize_filters(filters)
    filters.transform_keys(&:to_s).select { |key, _| VALID_FIELDS.include?(key) }
  end
end

def search_users(**params)
  search_params = SearchParameters.new(**params)
  puts "Search parameters: #{search_params.to_h}"
  
  conditions, parameters = search_params.to_sql_conditions
  puts "SQL: SELECT * FROM users WHERE #{conditions} ORDER BY #{search_params.order_by} #{search_params.order.upcase} LIMIT #{search_params.per_page} OFFSET #{search_params.offset}"
  puts "Parameters: #{parameters}"
  
  # Simulate database results
  { results: [], total: 0, page: search_params.page, per_page: search_params.per_page }
end

results = search_users(query: "alice", page: 2, per_page: 20, order: "desc", order_by: "created_at", role: "admin")

Common Pitfalls

Ruby method parameters present several common pitfalls that can lead to unexpected behavior, parameter conflicts, and runtime errors.

Mutable Default Parameters

Default parameter values are shared across method calls when they reference the same object. This creates unexpected state persistence between method invocations.

# Problematic implementation
def add_item(item, collection = [])
  collection << item
  puts "Collection now contains: #{collection}"
  collection
end

list1 = add_item("first")  # => ["first"]
list2 = add_item("second") # => ["first", "second"] - Unexpected!
list3 = add_item("third")  # => ["first", "second", "third"] - All calls share same array

# Correct implementation
def add_item_safely(item, collection = nil)
  collection ||= []
  collection << item
  puts "Collection now contains: #{collection}"
  collection
end

safe_list1 = add_item_safely("first")  # => ["first"]
safe_list2 = add_item_safely("second") # => ["second"] - Each call gets fresh array
safe_list3 = add_item_safely("third")  # => ["third"]

The same issue occurs with other mutable objects including hashes, custom objects, and even time-based defaults that should be evaluated per call.

# Time evaluation pitfall
class Logger
  # Wrong: timestamp evaluated once at class definition
  def log_wrong(message, timestamp = Time.now)
    puts "#{timestamp}: #{message}"
  end

  # Correct: timestamp evaluated per method call
  def log_correct(message, timestamp = nil)
    timestamp ||= Time.now
    puts "#{timestamp}: #{message}"
  end
end

logger = Logger.new
logger.log_wrong("First message")
sleep(2)
logger.log_wrong("Second message") # Same timestamp as first!

logger.log_correct("First message")
sleep(2) 
logger.log_correct("Second message") # Different timestamps

Parameter Order Dependencies

Mixing positional and keyword parameters creates order dependencies that can cause confusing errors and unexpected argument assignment.

# Problematic parameter mixing
def process_data(name, *items, required_option:, default_option: "default", **other_options)
  puts "Name: #{name}"
  puts "Items: #{items}"
  puts "Required: #{required_option}"
  puts "Default: #{default_option}"
  puts "Other: #{other_options}"
end

# This works as expected
process_data("test", "item1", "item2", required_option: "value")

# This causes confusion - where does "item3" go?
process_data("test", "item1", "item2", required_option: "value", extra: "data")

# Even more confusing with mixed argument types
def configure_service(name, port = 3000, *middleware, ssl: false, **options)
  # Parameter assignment becomes unclear
end

# Better design - clear separation
def configure_service_clear(name:, port: 3000, middleware: [], ssl: false, **options)
  puts "Service: #{name} on port #{port}"
  puts "Middleware: #{middleware}"
  puts "SSL: #{ssl}"
  puts "Options: #{options}"
end

configure_service_clear(
  name: "api",
  port: 8080,
  middleware: ["auth", "cors"],
  ssl: true,
  timeout: 30
)

Splat Parameter Gotchas

Splat parameters capture arguments in ways that can mask method signature errors and create unexpected parameter consumption.

def problematic_splat(first, *middle, last, **options)
  puts "First: #{first}"
  puts "Middle: #{middle}"
  puts "Last: #{last}"
  puts "Options: #{options}"
end

# Ruby requires at least 2 positional arguments for this to work
problematic_splat("only_one") # ArgumentError: wrong number of arguments

# Splat consumes more than expected
problematic_splat("a", "b", "c", "d", "e") 
# First: a
# Middle: ["b", "c", "d"]  
# Last: e
# Options: {}

# Keyword arguments don't go where expected
problematic_splat("a", "b", key: "value")
# ArgumentError - Ruby can't distinguish positional from keyword args

# Better approach with clearer intent
def clear_parameters(first:, last:, middle: [], **options)
  puts "First: #{first}"
  puts "Middle: #{middle}"  
  puts "Last: #{last}"
  puts "Options: #{options}"
end

clear_parameters(first: "a", last: "e", middle: ["b", "c", "d"], extra: "option")

Block Parameter Conflicts

Block parameters can conflict with local variables and create scoping issues that mask bugs or cause unexpected behavior.

def process_items(items, &processor)
  results = []
  items.each do |item|
    # Block parameter shadowing
    result = processor.call(item) if processor
    results << result
  end
  results
end

# Variable shadowing in block
items = ["a", "b", "c"]
results = []  # This variable gets shadowed

processed = process_items(items) do |item|
  results = item.upcase  # Creates new local variable, doesn't modify outer results
  results + "!"
end

puts processed  # => ["A!", "B!", "C!"]
puts results    # => [] - Original results unchanged due to shadowing

# Correct approach - avoid shadowing
def process_items_safe(items)
  results = []
  items.each do |item|
    result = yield(item) if block_given?
    results << result if result
  end
  results
end

outer_results = []
processed_safe = process_items_safe(items) do |item|
  transformed = item.upcase + "!"
  outer_results << "processed: #{transformed}"  # Explicit outer scope access
  transformed
end

Parameter Validation Edge Cases

Parameter validation often misses edge cases related to type coercion, nil handling, and boundary conditions.

class UserService
  def create_user(name:, email:, age: nil, tags: [], **metadata)
    # Insufficient validation
    raise ArgumentError, "Name required" if name.empty?
    raise ArgumentError, "Invalid email" unless email.include?("@")
    raise ArgumentError, "Age must be positive" if age && age <= 0
    
    # Edge cases not handled:
    # - name could be nil (nil.empty? raises NoMethodError)
    # - email could be just "@" 
    # - age could be a string that converts to number
    # - tags could be nil instead of array
    # - metadata could contain invalid keys/values
  end

  # Robust validation handles edge cases
  def create_user_robust(name:, email:, age: nil, tags: [], **metadata)
    # Handle nil and type issues
    name = name.to_s.strip
    raise ArgumentError, "Name cannot be blank" if name.empty?
    
    email = email.to_s.strip.downcase
    raise ArgumentError, "Invalid email format" unless valid_email?(email)
    
    if age
      age = Integer(age) rescue (raise ArgumentError, "Age must be a number")
      raise ArgumentError, "Age must be between 0 and 150" unless (0..150).cover?(age)
    end

    tags = Array(tags).map(&:to_s).uniq
    
    # Validate metadata keys
    invalid_keys = metadata.keys.reject { |k| k.to_s.match?(/\A[a-z_][a-z0-9_]*\z/) }
    raise ArgumentError, "Invalid metadata keys: #{invalid_keys}" unless invalid_keys.empty?

    { name: name, email: email, age: age, tags: tags, metadata: metadata }
  end

  private

  def valid_email?(email)
    email.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
  end
end

service = UserService.new

# These should all be handled gracefully
begin
  service.create_user_robust(name: nil, email: "invalid", age: "not_a_number")
rescue ArgumentError => e
  puts "Validation error: #{e.message}"
end

# Valid creation
user = service.create_user_robust(
  name: " Alice Smith ", 
  email: "ALICE@EXAMPLE.COM",
  age: "25",
  tags: ["admin", "admin", "user"], # Duplicates removed
  department: "engineering",
  hire_date: "2024-01-15"
)
puts user
# => {:name=>"Alice Smith", :email=>"alice@example.com", :age=>25, :tags=>["admin", "user"], :metadata=>{:department=>"engineering", :hire_date=>"2024-01-15"}}

Reference

Parameter Types

Parameter Type Syntax Behavior Example
Required Positional param Must be provided, matched by position def method(name)
Optional Positional param = default Uses default if not provided def method(name = "default")
Splat Arguments *params Captures variable positional args into array def method(*args)
Required Keyword param: Must be provided with keyword syntax def method(name:)
Optional Keyword param: default Uses default if not provided def method(name: "default")
Double Splat **params Captures variable keyword args into hash def method(**opts)
Block Parameter &block Captures block as Proc object def method(&block)

Parameter Order Rules

Ruby enforces strict parameter ordering in method signatures:

def method_signature(
  required_positional,           # 1. Required positional parameters
  optional_positional = default, # 2. Optional positional parameters  
  *splat_args,                  # 3. Splat parameter (captures remaining positional)
  required_keyword:,            # 4. Required keyword parameters
  optional_keyword: default,    # 5. Optional keyword parameters
  **double_splat,              # 6. Double splat (captures remaining keyword args)
  &block                       # 7. Block parameter
)

Method Call Argument Resolution

Call Pattern Argument Assignment Notes
method(a, b, c) Positional assignment left-to-right Matches required then optional positional params
method(a, key: value) Mixed positional and keyword Positional args first, then keyword args
method(*array) Array expansion to positional args Each array element becomes separate argument
method(**hash) Hash expansion to keyword args Each hash key-value becomes keyword argument
method(a, *array, **hash) Combined expansion Positional, then array expansion, then hash expansion

Block Handling Methods

Method Purpose Return Value Example
block_given? Check if block provided Boolean if block_given?
yield Execute block with arguments Block return value yield(arg1, arg2)
block.call Execute captured block Block return value block.call(arg1, arg2)
block.arity Number of block parameters Integer block.arity #=> 2
block.parameters Block parameter information Array of arrays block.parameters #=> [[:req, :x], [:opt, :y]]

Parameter Introspection

def sample_method(req, opt = nil, *args, keyword:, opt_kw: "default", **kwargs, &block)
  # Method implementation
end

method = method(:sample_method)
puts method.parameters
# => [[:req, :req], [:opt, :opt], [:rest, :args], [:keyreq, :keyword], [:key, :opt_kw], [:keyrest, :kwargs], [:block, :block]]

Parameter Type Symbols

Symbol Parameter Type Description
:req Required positional Must provide positional argument
:opt Optional positional Positional with default value
:rest Splat parameter Captures remaining positional args
:keyreq Required keyword Must provide keyword argument
:key Optional keyword Keyword with default value
:keyrest Double splat Captures remaining keyword args
:block Block parameter Captures block as Proc

Common Error Types

Error Class Cause Prevention
ArgumentError Wrong number of arguments Validate argument count matches signature
KeyError Missing required keyword argument Provide all required keyword parameters
TypeError Argument type mismatch Validate argument types before processing
NoMethodError Method called on nil from default parameter Use safe navigation or nil checks

Performance Considerations

Pattern Performance Impact Alternative
Mutable defaults Memory shared across calls Use nil default with `
Large splat arrays Array creation overhead Limit splat usage or use keyword args
Block conversion Proc object creation Use yield instead of &block.call when possible
Parameter validation Method call overhead Balance validation depth with performance needs
Deep parameter introspection Reflection overhead Cache method signatures when possible