CrackedRuby logo

CrackedRuby

Keyword Arguments

Overview

Ruby supports keyword arguments as named parameters passed to methods using the key: value syntax. This feature provides explicit parameter naming, optional parameters with defaults, and required keyword parameters. Ruby implements keyword arguments through the ** splat operator and special syntax in method signatures.

The keyword argument system handles parameter validation, default value assignment, and argument forwarding. Methods can accept keyword arguments alongside positional parameters, enabling flexible API designs. Ruby treats keyword arguments as a hash-like structure internally while maintaining compile-time parameter validation.

def create_user(name, email:, role: 'user', **options)
  puts "Name: #{name}, Email: #{email}, Role: #{role}"
  puts "Options: #{options}"
end

create_user('John', email: 'john@example.com', role: 'admin', active: true)
# Name: John, Email: john@example.com, Role: admin  
# Options: {:active=>true}

Keyword arguments separate into three categories: optional with defaults, required without defaults, and captured extra arguments. The ** operator captures undefined keyword arguments into a hash, similar to how * captures positional arguments into an array.

# Required keyword argument
def login(username:, password:)
  # Implementation
end

# Optional with default
def search(query:, limit: 10)
  # Implementation  
end

# Capturing extra arguments
def configure(**settings)
  settings.each { |key, value| puts "#{key}: #{value}" }
end

Ruby enforces keyword argument requirements at call time, raising ArgumentError for missing required parameters. The interpreter validates argument names against the method signature, preventing typos in parameter names.

Basic Usage

Methods define keyword arguments using the colon syntax in parameter lists. Required keyword arguments use name: without default values, while optional arguments use name: default_value. The method can mix positional and keyword arguments with specific ordering rules.

class DatabaseConnection
  def initialize(host:, port: 5432, username:, password:, ssl: false)
    @host = host
    @port = port  
    @username = username
    @password = password
    @ssl = ssl
  end
  
  def connect
    puts "Connecting to #{@host}:#{@port} (SSL: #{@ssl})"
  end
end

# Usage with mixed required and optional keyword arguments
db = DatabaseConnection.new(
  host: 'localhost',
  username: 'admin', 
  password: 'secret',
  ssl: true
)
db.connect
# Connecting to localhost:5432 (SSL: true)

The double splat operator ** captures additional keyword arguments into a hash parameter. This enables flexible APIs that accept both defined parameters and arbitrary options.

def send_notification(message:, **delivery_options)
  puts "Message: #{message}"
  
  if delivery_options[:email]
    puts "Email delivery to: #{delivery_options[:email]}"
  end
  
  if delivery_options[:sms]
    puts "SMS delivery to: #{delivery_options[:sms]}"
  end
  
  puts "Priority: #{delivery_options.fetch(:priority, 'normal')}"
end

send_notification(
  message: 'Server maintenance tonight',
  email: 'admin@company.com',
  priority: 'high'
)
# Message: Server maintenance tonight
# Email delivery to: admin@company.com
# Priority: high

Keyword arguments work with block-accepting methods and can delegate arguments to other methods. The delegation preserves argument names and types while enabling method composition.

def process_data(data, transform: :upcase, **options, &block)
  transformed = data.send(transform)
  
  if block_given?
    result = block.call(transformed, **options)
  else
    result = default_processing(transformed, **options)
  end
  
  result
end

def default_processing(data, prefix: '', suffix: '')
  "#{prefix}#{data}#{suffix}"
end

result = process_data('hello world', transform: :capitalize, prefix: '>> ') do |data, **opts|
  "PROCESSED: #{data}"
end
puts result
# PROCESSED: Hello world

Hash arguments convert automatically to keyword arguments when passed to methods expecting keywords. This conversion applies only when the hash appears as the last argument and contains symbol keys.

def create_account(name:, email:, type: 'standard')
  puts "Creating #{type} account for #{name} (#{email})"
end

# Direct keyword arguments
create_account(name: 'Alice', email: 'alice@example.com', type: 'premium')

# Hash conversion to keyword arguments  
account_data = { name: 'Bob', email: 'bob@example.com' }
create_account(**account_data, type: 'premium')
# Creating premium account for Bob (bob@example.com)

Advanced Usage

Keyword argument delegation enables sophisticated method composition patterns. Methods can forward keyword arguments while intercepting specific parameters or adding additional arguments. This pattern supports decorator and middleware implementations.

module Cacheable
  def with_cache(cache_key:, ttl: 3600, **kwargs)
    if cached_value = cache_get(cache_key)
      return cached_value
    end
    
    result = yield(**kwargs)
    cache_set(cache_key, result, ttl)
    result
  end
  
  private
  
  def cache_get(key)
    @cache ||= {}
    entry = @cache[key]
    return nil unless entry
    return nil if Time.now > entry[:expires]
    entry[:value]
  end
  
  def cache_set(key, value, ttl)
    @cache ||= {}
    @cache[key] = { value: value, expires: Time.now + ttl }
  end
end

class ApiClient
  include Cacheable
  
  def fetch_user_data(user_id:, include_posts: false, **options)
    with_cache(cache_key: "user_#{user_id}", **options) do |**forwarded|
      api_request("users/#{user_id}", include_posts: include_posts, **forwarded)
    end
  end
  
  private
  
  def api_request(endpoint, **params)
    # Simulate API call
    { endpoint: endpoint, params: params, timestamp: Time.now }
  end
end

client = ApiClient.new
data = client.fetch_user_data(
  user_id: 123,
  include_posts: true,
  ttl: 1800,
  format: 'json'
)

Method objects and lambda expressions support keyword arguments with identical semantics to regular methods. This enables functional programming patterns with named parameters and argument validation.

# Lambda with keyword arguments
user_validator = ->(name:, email:, age: nil, **metadata) do
  errors = []
  errors << 'Name required' if name.nil? || name.empty?
  errors << 'Invalid email' unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\z/i)
  errors << 'Age must be positive' if age && age <= 0
  
  {
    valid: errors.empty?,
    errors: errors,
    metadata: metadata
  }
end

# Method object creation
validate_method = user_validator.method(:call)

# Usage with various argument patterns
result1 = user_validator.call(name: 'John', email: 'john@example.com', source: 'signup')
result2 = validate_method.call(name: '', email: 'invalid', age: -5)

puts "Result 1 valid: #{result1[:valid]}, metadata: #{result1[:metadata]}"
puts "Result 2 errors: #{result2[:errors]}"

Metaprogramming with keyword arguments requires careful handling of method signatures and argument forwarding. Ruby provides introspection methods to examine parameter information and build dynamic method calls.

class DynamicProxy
  def initialize(target)
    @target = target
  end
  
  def method_missing(method_name, *args, **kwargs, &block)
    if @target.respond_to?(method_name)
      method_info = @target.method(method_name).parameters
      
      # Log method call with parameter details
      log_call(method_name, method_info, args, kwargs)
      
      # Forward the call preserving all argument types
      @target.send(method_name, *args, **kwargs, &block)
    else
      super
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    @target.respond_to?(method_name, include_private) || super
  end
  
  private
  
  def log_call(method_name, parameters, args, kwargs)
    puts "Calling #{method_name}:"
    
    parameters.each_with_index do |param, index|
      type, name = param
      case type
      when :req
        puts "  #{name}: #{args[index]} (required positional)"
      when :opt  
        puts "  #{name}: #{args[index]} (optional positional)" if args[index]
      when :keyreq
        puts "  #{name}: #{kwargs[name]} (required keyword)"
      when :key
        puts "  #{name}: #{kwargs[name]} (optional keyword)" if kwargs[name]
      when :keyrest
        puts "  **#{name}: #{kwargs.except(*parameters.map(&:last))} (keyword rest)"
      end
    end
  end
end

class Calculator
  def compute(base:, multiplier: 1, **options)
    result = base * multiplier
    result += options[:offset] if options[:offset]
    result
  end
end

calc = Calculator.new
proxy = DynamicProxy.new(calc)

result = proxy.compute(base: 10, multiplier: 3, offset: 5)
# Calling compute:
#   base: 10 (required keyword)  
#   multiplier: 3 (optional keyword)
#   **options: {:offset=>5} (keyword rest)
puts "Result: #{result}"  # Result: 35

Common Pitfalls

Keyword argument delegation creates subtle bugs when methods modify the arguments hash or when argument names clash. Ruby passes keyword arguments by reference, so modifications affect the original hash structure.

def process_order(items:, **options)
  # WRONG: Modifying options affects caller's hash
  options[:processed_at] = Time.now
  options[:total] = calculate_total(items)
  
  fulfill_order(items: items, **options)
end

def fulfill_order(items:, total:, processed_at:, **metadata)
  puts "Fulfilling order: #{items.count} items, total: $#{total}"
  puts "Processed at: #{processed_at}"
  puts "Metadata: #{metadata}"
end

# The caller's hash gets modified unexpectedly
order_options = { priority: 'high', shipping: 'express' }
items = ['book', 'pen']

process_order(items: items, **order_options)
puts "Original options modified: #{order_options}"
# Original options modified: {:priority=>"high", :shipping=>"express", :processed_at=>..., :total=>...}

# CORRECT: Create a copy before modification
def process_order_safe(items:, **options)
  safe_options = options.dup
  safe_options[:processed_at] = Time.now  
  safe_options[:total] = calculate_total(items)
  
  fulfill_order(items: items, **safe_options)
end

private

def calculate_total(items)
  items.count * 10  # Simple calculation
end

Required keyword arguments create confusing error messages when missing, especially in deeply nested method calls. Ruby reports the immediate method where the error occurs, not the original call site.

class ConfigurationBuilder
  def build(database:, **options)
    validate_database_config(database: database)
    create_config(database: database, **options)
  end
  
  private
  
  def validate_database_config(database:)
    check_connection_params(**database)  # Error occurs here
  end
  
  def check_connection_params(host:, username:, password:)
    # Validation logic
    puts "Checking connection to #{host} for #{username}"
  end
  
  def create_config(database:, **options)
    puts "Creating configuration with database: #{database}"
  end
end

builder = ConfigurationBuilder.new

# This will raise ArgumentError for missing password, but error location is confusing
begin
  builder.build(
    database: { host: 'localhost', username: 'admin' }  # missing password
  )
rescue ArgumentError => e
  puts "Error: #{e.message}"
  puts "Error occurred in: #{e.backtrace.first}"
end

# Better error handling with context
class ImprovedConfigurationBuilder
  def build(database:, **options)
    validate_required_database_fields(database)
    validate_database_config(database: database)
    create_config(database: database, **options)
  end
  
  private
  
  def validate_required_database_fields(database)
    required_fields = [:host, :username, :password]
    missing_fields = required_fields - database.keys
    
    unless missing_fields.empty?
      raise ArgumentError, "Missing required database configuration: #{missing_fields.join(', ')}"
    end
  end
  
  def validate_database_config(database:)
    check_connection_params(**database)
  end
  
  def check_connection_params(host:, username:, password:)
    puts "Checking connection to #{host} for #{username}"
  end
  
  def create_config(database:, **options)
    puts "Creating configuration"
  end
end

Hash-to-keyword conversion fails silently with string keys, leading to runtime errors when methods expect symbol-keyed keyword arguments. This commonly occurs when processing JSON or form data.

def create_user_account(name:, email:, role: 'user')
  puts "Creating account: #{name} (#{email}) as #{role}"
end

# JSON data typically has string keys
json_data = { 'name' => 'Alice', 'email' => 'alice@example.com', 'role' => 'admin' }

# This fails silently - no keyword arguments passed
begin
  create_user_account(**json_data)
rescue ArgumentError => e
  puts "Error with string keys: #{e.message}"
end

# Convert string keys to symbols for keyword argument compatibility
def symbolize_keys(hash)
  hash.transform_keys(&:to_sym)
end

# Correct approach
symbolized_data = symbolize_keys(json_data)
create_user_account(**symbolized_data)

# Alternative: Handle mixed key types gracefully
def flexible_user_creation(params)
  # Normalize all keys to symbols
  normalized = params.transform_keys(&:to_sym)
  create_user_account(**normalized)
end

flexible_user_creation(json_data)  # Works with string keys
flexible_user_creation(name: 'Bob', email: 'bob@example.com')  # Works with symbol keys

Reference

Method Definition Syntax

Syntax Pattern Description Example
name: Required keyword argument def method(name:); end
name: default Optional keyword with default def method(name: 'default'); end
**rest Capture extra keyword arguments def method(**opts); end
name:, **rest Required plus capture def method(name:, **opts); end

Parameter Ordering Rules

Ruby enforces strict parameter ordering in method definitions:

  1. Required positional parameters (name)
  2. Optional positional parameters (name = default)
  3. Splat parameter (*args)
  4. Required keyword parameters (name:)
  5. Optional keyword parameters (name: default)
  6. Double splat parameter (**kwargs)
  7. Block parameter (&block)
def complete_signature(req_pos, opt_pos = 'default', *args, req_kw:, opt_kw: 'default', **kwargs, &block)
  # Valid method signature following all ordering rules
end

Argument Passing Patterns

Call Pattern Description Requirement
method(key: value) Direct keyword argument Method defines key: parameter
method(**hash) Hash expansion to keywords Hash keys must be symbols
method(pos, key: value) Mixed positional and keyword Follows parameter order
method(**h1, **h2) Multiple hash expansion Ruby 3.0+ only

Error Types and Causes

Error Cause Example
ArgumentError (missing keyword: name) Required keyword not provided def m(name:); end; m()
ArgumentError (unknown keyword: invalid) Unexpected keyword argument def m(name:); end; m(name: 'x', invalid: 'y')
ArgumentError (wrong number of arguments) Positional argument mismatch Mixed with keyword issues

Introspection Methods

Methods for examining keyword argument information:

def sample_method(req:, opt: 'default', **rest)
  # Method body
end

method_obj = method(:sample_method)

# Get parameter information
params = method_obj.parameters
# => [[:keyreq, :req], [:key, :opt], [:keyrest, :rest]]

# Parameter types:
# :keyreq - Required keyword argument
# :key - Optional keyword argument  
# :keyrest - Keyword rest parameter (**kwargs)

# Check if method accepts keyword arguments
has_keywords = params.any? { |type, _| [:keyreq, :key, :keyrest].include?(type) }

# Get required keyword parameter names
required_keywords = params.select { |type, _| type == :keyreq }.map(&:last)
# => [:req]

# Get optional keyword parameter names
optional_keywords = params.select { |type, _| type == :key }.map(&:last) 
# => [:opt]

Compatibility Notes

Ruby versions handle keyword arguments differently:

  • Ruby 2.6 and earlier: Automatic conversion between hash and keyword arguments
  • Ruby 2.7: Deprecation warnings for automatic conversion
  • Ruby 3.0+: Strict separation between positional and keyword arguments
  • Ruby 3.0+: Support for multiple hash expansion (**h1, **h2)

For cross-version compatibility, explicitly separate positional and keyword arguments and avoid relying on automatic hash conversion.