CrackedRuby CrackedRuby

Parameter Passing Mechanisms

Overview

Parameter passing mechanisms determine how arguments flow from a caller to a function or method. When code invokes a function with arguments, the language runtime must decide whether to transmit the actual data, a copy of the data, or a reference to the data's memory location. This decision affects program behavior, performance, memory usage, and the ability of functions to modify caller data.

Different programming languages adopt different strategies. C passes arguments by value, making copies of primitive types. C++ offers both pass by value and pass by reference through reference parameters. Python and Ruby use pass by object reference, where references to objects are passed by value. Understanding these mechanisms explains why mutations inside functions sometimes affect external state and sometimes don't.

The choice of parameter passing mechanism influences API design, memory efficiency, and program correctness. Functions that accept large data structures benefit from reference-based passing to avoid expensive copies. Functions that need to modify caller state require either reference parameters or return values. Immutable objects reduce the complexity of reasoning about parameter passing because they cannot be modified regardless of the mechanism.

def modify_array(arr)
  arr << 4  # Modifies the original array
end

numbers = [1, 2, 3]
modify_array(numbers)
numbers
# => [1, 2, 3, 4]

def reassign_array(arr)
  arr = [9, 8, 7]  # Creates new binding, doesn't affect caller
end

original = [1, 2, 3]
reassign_array(original)
original
# => [1, 2, 3]

Key Principles

Pass by Value transmits a copy of the argument to the function. The function receives an independent copy of the data, and modifications to the parameter do not affect the original argument. This mechanism guarantees isolation between caller and callee but incurs copying costs for large data structures. C uses pass by value for all parameters, requiring explicit pointer usage for reference semantics.

Pass by Reference transmits the memory address of the argument. The function parameter becomes an alias for the original argument, and any modifications affect the caller's data directly. This mechanism enables functions to modify caller state efficiently without copying data. C++ implements pass by reference through reference parameters (int& x), while languages like Fortran use pass by reference by default for certain types.

Pass by Object Reference (also called pass by sharing or pass by assignment) transmits a reference to an object, but the reference itself is passed by value. The function cannot rebind the caller's variable to a different object, but it can mutate the object through the reference. Python, Ruby, Java, and JavaScript use this mechanism. The distinction between mutable and immutable objects becomes significant because immutable objects cannot be modified, mimicking pass by value semantics.

Reference Semantics vs Value Semantics describes whether operations on a type affect the original data or a copy. Types with value semantics (integers, structs copied on assignment) behave as if passed by value regardless of the underlying mechanism. Types with reference semantics (objects, arrays) maintain identity across assignments and parameter passing.

Aliasing occurs when multiple references point to the same object. Parameter passing creates aliasing when a function parameter and an external variable reference the same object. Aliasing complicates reasoning about program state because mutations through one reference affect all other references. Immutability eliminates aliasing concerns for the objects themselves, though mutable containers holding immutable objects still create aliasing scenarios.

Rebinding vs Mutation represents a critical distinction in reference-based languages. Rebinding assigns a new object to a variable, changing what the variable references. Mutation modifies the existing object without changing references. Parameter passing mechanisms affect rebinding behavior (functions cannot rebind caller variables in pass by object reference) but not mutation (functions can mutate objects).

# Demonstrates rebinding vs mutation
def demonstrate_rebinding(x)
  x = x + [4, 5]  # Creates new array, rebinds x
end

def demonstrate_mutation(x)
  x.concat([4, 5])  # Mutates existing array
end

arr1 = [1, 2, 3]
demonstrate_rebinding(arr1)
arr1  # => [1, 2, 3] - rebinding didn't affect caller

arr2 = [1, 2, 3]
demonstrate_mutation(arr2)
arr2  # => [1, 2, 3, 4, 5] - mutation affected caller

The object identity can be tested to verify parameter passing behavior. Objects that maintain identity across function calls indicate reference transmission, while objects that change identity indicate copying.

Ruby Implementation

Ruby uses pass by object reference exclusively. When a method is called, Ruby passes references to objects, but these references are passed by value. The method receives a copy of the reference, not a copy of the object. This means methods can mutate objects through their parameters but cannot reassign the caller's variables.

All Ruby values are objects, including integers and symbols. However, some objects are immutable (integers, symbols, frozen strings), which gives them value-like semantics despite being passed by reference. Attempting to "modify" an immutable object actually creates a new object, leaving the original unchanged.

def modify_string(str)
  str << " World"  # Mutates the string object
end

def reassign_string(str)
  str = str + " World"  # Creates new string, rebinds local str
end

greeting = "Hello"
modify_string(greeting)
greeting  # => "Hello World"

greeting = "Hello"
reassign_string(greeting)
greeting  # => "Hello" - reassignment didn't affect caller

The object_id method returns a unique identifier for each object, demonstrating whether operations create new objects or modify existing ones:

def show_object_ids(obj)
  puts "Inside method: #{obj.object_id}"
  obj << " modified"
  puts "After modification: #{obj.object_id}"
end

str = "Original"
puts "Before call: #{str.object_id}"
show_object_ids(str)
puts "After call: #{str.object_id}"
# All object_ids are identical - same object throughout

Ruby's immutable types include Integer, Float, Symbol, and TrueClass/FalseClass/NilClass. Operations on these types always return new objects:

def try_modify_integer(n)
  n += 10  # Creates new Integer object
  n
end

x = 5
result = try_modify_integer(x)
x       # => 5
result  # => 15

Strings in Ruby are mutable by default but can be frozen. Frozen strings behave like immutable objects, raising exceptions on modification attempts:

def modify_frozen(str)
  str << " World"
end

frozen_str = "Hello".freeze
modify_frozen(frozen_str)
# Raises FrozenError: can't modify frozen String

Arrays and hashes are mutable containers. Methods can modify their contents through parameter references:

def modify_array_contents(arr)
  arr[0] = 100       # Modifies element
  arr << 200         # Appends element
end

def modify_hash_contents(hash)
  hash[:new_key] = "new_value"
  hash[:existing] = "modified"
end

numbers = [1, 2, 3]
modify_array_contents(numbers)
numbers  # => [100, 2, 3, 200]

data = { existing: "original" }
modify_hash_contents(data)
data  # => { existing: "modified", new_key: "new_value" }

The dup and clone methods create shallow copies, giving callers control over whether functions affect original objects:

def destructive_operation(arr)
  arr.clear
  arr << 999
end

original = [1, 2, 3]
destructive_operation(original.dup)
original  # => [1, 2, 3] - unaffected because dup created copy

Method parameters with default values are evaluated at call time, and the same default object is used across calls:

def append_to_list(item, list = [])
  list << item
end

append_to_list(1)  # => [1]
append_to_list(2)  # => [1, 2] - same array object!
append_to_list(3)  # => [1, 2, 3]

# Correct approach
def append_to_list_safe(item, list = nil)
  list ||= []
  list << item
end

Keyword arguments follow the same pass by object reference semantics:

def process_options(name:, settings: {})
  settings[:processed] = true
  "Processed #{name}"
end

opts = { color: "blue" }
process_options(name: "widget", settings: opts)
opts  # => { color: "blue", processed: true }

Practical Examples

Example 1: Building Fluent Interfaces

Methods that mutate and return self create fluent interfaces, taking advantage of Ruby's reference passing:

class QueryBuilder
  def initialize
    @conditions = []
    @order = nil
  end

  def where(condition)
    @conditions << condition
    self  # Returns reference to same object
  end

  def order_by(field)
    @order = field
    self
  end

  def to_sql
    sql = "SELECT * FROM table"
    sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
    sql += " ORDER BY #{@order}" if @order
    sql
  end
end

query = QueryBuilder.new
query.where("age > 18").where("active = true").order_by("name")
query.to_sql
# => "SELECT * FROM table WHERE age > 18 AND active = true ORDER BY name"

Example 2: Accumulator Pattern with Side Effects

Functions that accumulate state by modifying passed collections:

def collect_errors(validation_result, errors)
  errors << "Name cannot be blank" if validation_result[:name].nil?
  errors << "Email is invalid" unless validation_result[:email]&.include?("@")
  errors << "Age must be positive" if validation_result[:age]&.negative?
end

def validate_user(user_data)
  errors = []
  collect_errors(user_data, errors)
  
  if errors.empty?
    { valid: true }
  else
    { valid: false, errors: errors }
  end
end

result = validate_user({ name: nil, email: "invalid", age: -5 })
# => { valid: false, errors: ["Name cannot be blank", "Email is invalid", "Age must be positive"] }

Example 3: Destructive vs Non-Destructive Operations

Designing methods with clear semantics about mutation:

class TextProcessor
  # Non-destructive: returns new string
  def self.sanitize(text)
    text.gsub(/[<>]/, '')
  end

  # Destructive: modifies in place
  def self.sanitize!(text)
    text.gsub!(/[<>]/, '')
    text
  end

  # Guard against nil
  def self.sanitize_safe(text)
    return "" if text.nil?
    text.gsub(/[<>]/, '')
  end
end

original = "Hello <world>"
cleaned = TextProcessor.sanitize(original)
original  # => "Hello <world>" - unchanged
cleaned   # => "Hello world"

content = "Test <script>"
TextProcessor.sanitize!(content)
content   # => "Test script" - modified in place

Example 4: Shared State in Callbacks

Callbacks that modify shared state through parameter references:

class EventProcessor
  def process_events(events, handlers, context)
    events.each do |event|
      handlers.each do |handler|
        handler.call(event, context)
      end
    end
  end
end

# Context accumulates state across all callbacks
context = { processed: 0, errors: [] }

handlers = [
  ->(event, ctx) { ctx[:processed] += 1 },
  ->(event, ctx) { 
    ctx[:errors] << "Invalid event" if event[:data].nil?
  }
]

events = [
  { type: "create", data: { name: "Test" } },
  { type: "update", data: nil },
  { type: "delete", data: { id: 123 } }
]

processor = EventProcessor.new
processor.process_events(events, handlers, context)

context
# => { processed: 3, errors: ["Invalid event"] }

Example 5: Protecting Against Unintended Mutation

Using freezing and duplication to control mutability:

class ConfigurationManager
  def initialize(defaults)
    @defaults = defaults.freeze  # Prevents modification
  end

  def get_config(overrides = {})
    @defaults.dup.merge(overrides)  # Returns mutable copy with overrides
  end

  def apply_settings(config)
    # Create frozen copy to prevent external modification
    @current_config = config.dup.freeze
  end
end

defaults = { timeout: 30, retries: 3 }
manager = ConfigurationManager.new(defaults)

config = manager.get_config(timeout: 60)
config[:timeout] = 90  # OK, modifying copy
defaults[:timeout]     # => 30, still unchanged

manager.apply_settings(config)
# Attempting to modify @current_config would raise FrozenError

Common Patterns

Defensive Copying Pattern

Creating copies of mutable parameters to prevent external modifications:

class UserRegistry
  def initialize
    @users = []
  end

  # Accept array but work with copy
  def bulk_add(users)
    users_copy = users.dup
    users_copy.each do |user|
      @users << validate_and_normalize(user)
    end
  end

  # Return copy to prevent external modification
  def all_users
    @users.dup
  end

  private

  def validate_and_normalize(user)
    # Validation and normalization logic
    user.dup
  end
end

Builder Pattern with Chaining

Methods return self to enable fluent interfaces:

class EmailBuilder
  def initialize
    @to = []
    @subject = ""
    @body = ""
  end

  def to(*addresses)
    @to.concat(addresses)
    self
  end

  def subject(text)
    @subject = text
    self
  end

  def body(text)
    @body = text
    self
  end

  def send
    # Send email logic
    { to: @to, subject: @subject, body: @body }
  end
end

email = EmailBuilder.new
  .to("user@example.com")
  .subject("Welcome")
  .body("Thank you for signing up")
  .send

Accumulator Parameter Pattern

Passing a mutable accumulator through recursive or iterative calls:

def flatten_nested_array(array, accumulator = [])
  array.each do |element|
    if element.is_a?(Array)
      flatten_nested_array(element, accumulator)
    else
      accumulator << element
    end
  end
  accumulator
end

nested = [1, [2, [3, 4], 5], 6, [7]]
result = flatten_nested_array(nested)
# => [1, 2, 3, 4, 5, 6, 7]

Command Pattern with Mutable State

Commands that encapsulate operations on shared mutable state:

class Command
  def execute(state)
    raise NotImplementedError
  end
end

class AddItemCommand < Command
  def initialize(item)
    @item = item
  end

  def execute(state)
    state[:items] << @item
  end
end

class RemoveItemCommand < Command
  def initialize(item)
    @item = item
  end

  def execute(state)
    state[:items].delete(@item)
  end
end

# State is mutated by commands
state = { items: [] }
commands = [
  AddItemCommand.new("apple"),
  AddItemCommand.new("banana"),
  RemoveItemCommand.new("apple")
]

commands.each { |cmd| cmd.execute(state) }
state[:items]  # => ["banana"]

Freezing for Immutability

Using freeze to prevent modification after initialization:

class ImmutablePoint
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
    freeze  # Prevent any further modifications
  end

  def translate(dx, dy)
    # Return new instance instead of modifying
    ImmutablePoint.new(@x + dx, @y + dy)
  end
end

point = ImmutablePoint.new(10, 20)
new_point = point.translate(5, 5)
point.x      # => 10
new_point.x  # => 15

Design Considerations

Mutability vs Immutability Trade-offs

Mutable parameters enable efficient in-place modifications but create aliasing risks. Methods accepting mutable parameters must document whether they mutate arguments. Callers must understand whether passed objects will be modified. Immutable parameters eliminate aliasing concerns but require object creation for each modification, increasing memory allocation and garbage collection pressure.

For small, frequently created objects, immutability simplifies reasoning without significant performance cost. For large data structures or performance-critical code, in-place mutation reduces overhead. Ruby's convention of bang methods (methods ending in !) indicates destructive operations, helping callers understand mutation semantics.

Return Value vs Parameter Modification

Functions can communicate results through return values or by modifying parameters. Return values make data flow explicit and support functional programming patterns. Parameter modification enables functions to update multiple values or avoid return value overhead. Ruby's support for multiple return values through array destructuring makes return values more attractive for multi-value results:

# Return values approach
def parse_coordinates(text)
  parts = text.split(',')
  [parts[0].to_i, parts[1].to_i]
end

x, y = parse_coordinates("10,20")

# Parameter modification approach
def parse_coordinates_into(text, result)
  parts = text.split(',')
  result[:x] = parts[0].to_i
  result[:y] = parts[1].to_i
end

coords = {}
parse_coordinates_into("10,20", coords)

Deep vs Shallow Copying

dup and clone create shallow copies, copying the object structure but not nested objects. Modifications to nested objects affect both the original and the copy. Deep copying requires recursive duplication of all nested objects:

def deep_copy(obj)
  Marshal.load(Marshal.dump(obj))
end

original = { users: [{ name: "Alice" }] }
shallow = original.dup
deep = deep_copy(original)

shallow[:users] << { name: "Bob" }
original[:users]  # => [{ name: "Alice" }, { name: "Bob" }]

deep[:users] << { name: "Charlie" }
original[:users]  # => [{ name: "Alice" }, { name: "Bob" }]

Deep copying incurs significant overhead and may not work for objects containing non-serializable data (procs, lambdas, IO objects). Design APIs to minimize the need for deep copying through careful ownership and mutation rules.

Keyword Arguments vs Positional Parameters

Keyword arguments make mutation semantics clearer by naming the parameters being modified. They also prevent accidental parameter reordering:

def update_user(id, name: nil, email: nil, metadata: {})
  # Clear which parameters are optional and mutable
  metadata[:updated_at] = Time.now
  # Update logic
end

# Caller clearly sees what's being passed and potentially modified
user_metadata = {}
update_user(123, name: "Alice", metadata: user_metadata)
user_metadata[:updated_at]  # Now contains timestamp

Frozen Collections with Mutable Elements

Freezing a collection prevents adding or removing elements but does not freeze contained objects:

array = ["hello", "world"].freeze
array << "test"  # Raises FrozenError

array[0] << " there"  # Works! String is mutable
array  # => ["hello there", "world"]

To create deeply frozen structures, recursively freeze all nested objects or use immutable data structure libraries.

Common Pitfalls

Default Parameter Object Reuse

Ruby evaluates default parameter values once at method definition, not at each call. Mutable default objects are shared across all calls:

# Problematic
def add_to_collection(item, collection = [])
  collection << item
end

add_to_collection(1)  # => [1]
add_to_collection(2)  # => [1, 2] - unexpected!

# Correct
def add_to_collection(item, collection = nil)
  collection ||= []
  collection << item
end

Modifying Method Parameters

Modifying parameters inside methods creates surprising behavior for callers:

def process_name(name)
  name.upcase!  # Modifies caller's string
  name.strip!
  name
end

user_name = "  alice  "
result = process_name(user_name)
user_name  # => "ALICE" - unexpected modification!

# Better approach
def process_name(name)
  name.upcase.strip  # Returns new string
end

Assuming Integer Mutability

Attempting to modify integers fails silently because operations create new objects:

def increment(n)
  n += 1  # Creates new Integer, doesn't affect caller
end

counter = 5
increment(counter)
counter  # => 5, still unchanged

# Must use return value
counter = increment(counter)  # Now counter is 6

Mixing Destructive and Non-Destructive Methods

Inconsistent naming leads to confusion about whether methods mutate:

text = "hello"

# Non-destructive, returns new string
text.upcase  # => "HELLO"
text         # => "hello"

# Destructive, modifies in place
text.upcase!  # => "HELLO"
text          # => "HELLO"

# Inconsistent - concat is destructive but doesn't have !
text.concat(" world")
text  # => "HELLO world" - modified

Frozen String Modifications

Frozen strings raise errors on mutation attempts:

CONSTANT = "immutable"

def modify_string(str)
  str << " modified"
end

modify_string(CONSTANT)
# Raises FrozenError: can't modify frozen String

# Check frozen status
if CONSTANT.frozen?
  new_str = CONSTANT + " modified"
else
  CONSTANT << " modified"
end

Hash Modification Through Parameters

Hashes passed to methods can be unexpectedly modified:

def set_defaults(options)
  options[:timeout] ||= 30
  options[:retries] ||= 3
end

user_options = { timeout: 60 }
set_defaults(user_options)
user_options  # => { timeout: 60, retries: 3 } - modified!

# Avoid mutation
def set_defaults(options)
  { timeout: 30, retries: 3 }.merge(options)
end

Shallow Copy Surprise

Shallow copies share nested objects with the original:

original = { config: { timeout: 30 } }
copy = original.dup

copy[:config][:timeout] = 60
original[:config][:timeout]  # => 60, also changed!

# Need deep copy
require 'json'
deep_copy = JSON.parse(JSON.generate(original))
# or
deep_copy = Marshal.load(Marshal.dump(original))

Reference

Parameter Passing Semantics

Mechanism Definition Variable Rebinding Object Mutation Languages
Pass by Value Copy of data passed to function No effect on caller No effect on caller C, Go
Pass by Reference Memory address passed to function Affects caller variable Affects caller object C++ with &, Fortran
Pass by Object Reference Reference passed by value No effect on caller Affects caller object Ruby, Python, Java, JavaScript

Ruby Mutability Reference

Type Mutable Mutating Operations Creating New Objects
Integer No N/A All arithmetic operations
Float No N/A All arithmetic operations
Symbol No N/A N/A
String Yes concat, <<, upcase!, gsub! +, upcase, gsub
Array Yes push, <<, pop, concat +, -, map, select
Hash Yes []= assignment, merge!, update merge, select, transform_values
Range No N/A N/A
TrueClass/FalseClass/NilClass No N/A N/A

Common Method Behaviors

Method Pattern Behavior Example
method Non-destructive, returns new object String#upcase
method! Destructive, modifies in place String#upcase!
method= Setter, modifies object state attr_accessor
method? Predicate, returns boolean Array#empty?

Copying Methods

Method Behavior Preserves Frozen State Copies Singleton Methods
dup Shallow copy No No
clone Shallow copy Yes Yes
Marshal.load/dump Deep copy Yes No

Protection Patterns

Pattern Implementation Use Case
Defensive Copy In parameter.dup at method entry Prevent caller from affecting internal state
Defensive Copy Out @ivar.dup in getter Prevent external modification of internal state
Freeze After Initialize freeze in initialize Create immutable objects
Frozen Default Parameters CONSTANT.dup as default Prevent default object mutation

Testing for Mutability

Method Purpose Returns
frozen? Check if object is frozen true or false
object_id Get unique object identifier Integer
equal? Check object identity true or false
== Check object equality true or false

Parameter Declaration Styles

Style Syntax Characteristics
Positional def method(a, b) Order matters, all or none
Optional Positional def method(a, b = default) Defaults for trailing params
Splat def method(*args) Variable number of positional
Keyword def method(a:, b:) Named, order independent
Optional Keyword def method(a: 1, b: 2) Named with defaults
Keyword Splat def method(**kwargs) Variable number of keyword args
Block def method(&block) Captures block as proc