CrackedRuby logo

CrackedRuby

Object Inspection Methods

Ruby's object inspection methods provide developers with tools for examining object state, structure, and behavior during development and debugging.

Metaprogramming Reflection and Introspection
5.1.1

Overview

Ruby provides several built-in methods for object inspection that convert objects into readable string representations. The core inspection methods include inspect, to_s, p, pp, and display, each serving different purposes in object examination and debugging workflows.

The inspect method returns a string representation of an object that aims to be unambiguous and suitable for debugging. Ruby calls inspect internally when displaying objects in interactive sessions and when objects appear in arrays or hashes during output.

arr = [1, "hello", :symbol, nil]
arr.inspect
# => "[1, \"hello\", :symbol, nil]"

hash = { name: "Alice", age: 30 }
hash.inspect  
# => "{:name=>\"Alice\", :age=>30}"

The to_s method provides a string representation intended for end-user display rather than debugging. Many Ruby classes override to_s to provide human-readable output while leaving inspect for technical representation.

time = Time.new(2024, 3, 15, 14, 30)
time.to_s     # => "2024-03-15 14:30:00 -0500"
time.inspect  # => "2024-03-15 14:30:00.000000000 -0500"

Ruby also provides convenience methods like p for quick debugging output and pp for pretty-printed formatted output of complex data structures. The display method writes the object's string representation directly to an output stream without adding newlines.

Object inspection forms the foundation of Ruby's debugging and development experience, enabling developers to examine program state and verify object behavior during execution.

Basic Usage

The p method combines object inspection with immediate output, making it invaluable for debugging. It calls inspect on each argument and prints the result to standard output, returning the original object.

def calculate_total(items)
  total = 0
  items.each do |item|
    p item  # Debug output
    total += item[:price] * item[:quantity]
  end
  total
end

items = [
  { name: "Book", price: 15.99, quantity: 2 },
  { name: "Pen", price: 1.50, quantity: 5 }
]

result = calculate_total(items)
# Output: {:name=>"Book", :price=>15.99, :quantity=>2}
#         {:name=>"Pen", :price=>1.50, :quantity=>5}

The inspect method provides detailed object representation that includes class information and internal state. Different Ruby classes implement inspect to show relevant details for debugging purposes.

str = "Hello\nWorld"
str.inspect  # => "\"Hello\\nWorld\""

regexp = /[a-z]+/i
regexp.inspect  # => "/[a-z]+/i"

range = (1..10)
range.inspect  # => "1..10"

For complex data structures, the pp method from the PP library provides formatted output with proper indentation and line breaks. This method proves especially valuable when examining nested structures.

require 'pp'

data = {
  users: [
    { id: 1, name: "Alice", preferences: { theme: "dark", lang: "en" } },
    { id: 2, name: "Bob", preferences: { theme: "light", lang: "es" } }
  ],
  settings: { timeout: 300, retry_limit: 3 }
}

pp data
# Output:
# {:users=>
#   [{:id=>1, :name=>"Alice", :preferences=>{:theme=>"dark", :lang=>"en"}},
#    {:id=>2, :name=>"Bob", :preferences=>{:theme=>"light", :lang=>"es"}}],
#  :settings=>{:timeout=>300, :retry_limit=>3}}

The display method writes object representations directly to output streams without automatic newlines, providing precise control over output formatting.

numbers = [1, 2, 3, 4, 5]
numbers.each { |n| n.display; print " " }
# Output: 1 2 3 4 5

File.open("debug.log", "w") do |file|
  data = { timestamp: Time.now, event: "user_login" }
  data.display(file)
end

Advanced Usage

Custom classes can override inspect and to_s to provide meaningful representations tailored to specific use cases. The inspect method should return a string that helps developers understand the object's state and identity.

class BankAccount
  def initialize(account_number, balance)
    @account_number = account_number
    @balance = balance
  end
  
  def inspect
    "#<BankAccount:#{object_id} @account_number=#{@account_number.inspect} @balance=#{@balance.inspect}>"
  end
  
  def to_s
    "Account #{@account_number[-4..-1].rjust(4, '*')}: $#{@balance}"
  end
end

account = BankAccount.new("1234567890", 1500.75)
account.inspect  # => "#<BankAccount:70245851180460 @account_number=\"1234567890\" @balance=1500.75>"
account.to_s     # => "Account 7890: $1500.75"
p account        # Uses inspect method
puts account     # Uses to_s method

Metaprogramming techniques enable dynamic inspection capabilities that adapt based on object state or configuration. Classes can implement conditional inspection behavior or generate inspection output programmatically.

class ConfigurableInspection
  attr_accessor :debug_mode
  
  def initialize(data)
    @data = data
    @debug_mode = false
  end
  
  def inspect
    if @debug_mode
      instance_variables.map do |var|
        "#{var}=#{instance_variable_get(var).inspect}"
      end.join(" ")
    else
      "#<#{self.class.name}:#{object_id}>"
    end
  end
end

obj = ConfigurableInspection.new({ key: "value", count: 42 })
obj.inspect  # => "#<ConfigurableInspection:70245851180460>"

obj.debug_mode = true
obj.inspect  # => "@data={:key=>\"value\", :count=>42} @debug_mode=true"

Ruby's inspection methods integrate with custom formatting systems for specialized output requirements. Classes can implement inspection methods that generate different formats based on context or output destination.

class APIResponse
  def initialize(status, data, headers = {})
    @status = status
    @data = data  
    @headers = headers
    @timestamp = Time.now
  end
  
  def inspect
    "#<APIResponse status=#{@status} data_size=#{@data.to_s.length} headers=#{@headers.length}>"
  end
  
  def detailed_inspect
    {
      status: @status,
      data: @data,
      headers: @headers,
      timestamp: @timestamp,
      object_id: object_id
    }.inspect
  end
  
  def json_inspect
    require 'json'
    {
      status: @status,
      data: @data,
      timestamp: @timestamp.iso8601
    }.to_json
  end
end

response = APIResponse.new(200, { users: ["Alice", "Bob"] }, { "Content-Type" => "application/json" })
response.inspect          # => "#<APIResponse status=200 data_size=23 headers=1>"
response.detailed_inspect # => "{:status=>200, :data=>{:users=>[\"Alice\", \"Bob\"]}, ...}"
response.json_inspect     # => "{\"status\":200,\"data\":{\"users\":[\"Alice\",\"Bob\"]},\"timestamp\":\"2024-03-15T19:30:00Z\"}"

Advanced inspection techniques include conditional output, sensitive data masking, and integration with logging systems. These patterns enable production-safe inspection methods that protect sensitive information while providing useful debugging output.

class User
  attr_reader :id, :username, :email
  
  def initialize(id, username, email, password)
    @id = id
    @username = username
    @email = email
    @password = password
  end
  
  def inspect
    if ENV['RAILS_ENV'] == 'production'
      "#<User id=#{@id} username=#{@username.inspect}>"
    else
      "#<User id=#{@id} username=#{@username.inspect} email=#{@email.inspect}>"
    end
  end
  
  def safe_inspect
    attrs = instance_variables.reject { |var| var.to_s.include?('password') }
    attrs.map { |var| "#{var}=#{instance_variable_get(var).inspect}" }.join(" ")
  end
end

Error Handling & Debugging

Object inspection methods can raise exceptions when dealing with problematic objects or circular references. The most common issue occurs with objects that contain circular references, causing infinite recursion during string conversion.

class Node
  attr_accessor :value, :next_node
  
  def initialize(value)
    @value = value
    @next_node = nil
  end
end

# Create circular reference
node1 = Node.new("first")
node2 = Node.new("second") 
node1.next_node = node2
node2.next_node = node1

# This will raise SystemStackError due to infinite recursion
begin
  node1.inspect
rescue SystemStackError => e
  puts "Circular reference detected: #{e.message}"
end

Ruby provides mechanisms to detect and handle circular references during inspection. The pp library includes built-in circular reference detection that prevents infinite loops during pretty printing.

require 'pp'

# Create circular structure
hash1 = { name: "hash1" }
hash2 = { name: "hash2" }
hash1[:ref] = hash2
hash2[:ref] = hash1

# pp handles circular references automatically
pp hash1
# Output: {:name=>"hash1", :ref=>{:name=>"hash2", :ref=>#<Object:0x... @name="hash1", @ref=...>}}

Custom inspection methods should implement safeguards against problematic scenarios such as missing instance variables, nil values, or objects in invalid states. Defensive programming techniques prevent inspection failures during debugging sessions.

class SafeInspection
  def initialize(data)
    @data = data
    @processed = false
  end
  
  def inspect
    begin
      state = @processed ? "processed" : "unprocessed"
      data_info = @data.nil? ? "no data" : "#{@data.class}(#{@data.size rescue 'unknown size'})"
      "#<#{self.class.name}:#{object_id} state=#{state} data=#{data_info}>"
    rescue => e
      "#<#{self.class.name}:#{object_id} inspection_error=#{e.class.name}>"
    end
  end
end

# Test with various problematic scenarios
obj1 = SafeInspection.new(nil)
obj1.inspect  # => "#<SafeInspection:70245851180460 state=unprocessed data=no data>"

obj2 = SafeInspection.new("not enumerable")
obj2.inspect  # => "#<SafeInspection:70245851180461 state=unprocessed data=String(unknown size)>"

Debugging complex inspection issues often requires examining the method resolution order and understanding which classes provide specific inspection implementations. Ruby's introspection methods help identify inspection method sources.

class DebugInspection
  def self.trace_inspection_methods(obj)
    methods = [:inspect, :to_s, :display]
    methods.each do |method|
      method_obj = obj.method(method)
      puts "#{method}: defined in #{method_obj.owner}"
      puts "  Source location: #{method_obj.source_location}"
      puts "  Parameters: #{method_obj.parameters}"
    end
  end
end

# Trace inspection methods for different object types
DebugInspection.trace_inspection_methods("string")
DebugInspection.trace_inspection_methods([1, 2, 3])
DebugInspection.trace_inspection_methods({ key: :value })

Common Pitfalls

The distinction between inspect and to_s frequently causes confusion among Ruby developers. Many assume these methods produce identical output, but they serve different purposes and often return different strings.

class Product
  def initialize(name, price)
    @name = name
    @price = price
  end
  
  def to_s
    "#{@name} - $#{@price}"
  end
  
  # No custom inspect method - uses default
end

product = Product.new("Laptop", 999.99)
product.to_s      # => "Laptop - $999.99"
product.inspect   # => "#<Product:0x00007f8b1c0a1234 @name=\"Laptop\", @price=999.99>"

puts product      # Uses to_s: "Laptop - $999.99"
p product         # Uses inspect: #<Product:0x00007f8b1c0a1234 @name="Laptop", @price=999.99>

String escaping in inspect output catches developers off guard when dealing with special characters, newlines, and quotes. The inspect method produces Ruby literal representations that include necessary escape sequences.

text_with_escapes = "Line 1\nLine 2\tTabbed\nQuote: \"Hello\""
puts text_with_escapes
# Output: Line 1
#         Line 2	Tabbed
#         Quote: "Hello"

text_with_escapes.inspect
# => "Line 1\\nLine 2\\tTabbed\\nQuote: \\\"Hello\\\""

# Common mistake: expecting inspect output to match display output
filename = "report\n2024.txt"
puts "Processing: #{filename.inspect}"  # Correct: shows escaped newline
puts "Processing: #{filename}"          # Incorrect: literal newline in output

Infinite recursion occurs when objects contain circular references and custom inspection methods lack proper safeguards. This issue commonly appears in linked data structures, graph representations, and objects with bidirectional relationships.

class LinkedNode
  attr_accessor :value, :parent, :children
  
  def initialize(value)
    @value = value
    @parent = nil
    @children = []
  end
  
  # Dangerous: can cause infinite recursion
  def inspect_unsafe
    "#<LinkedNode value=#{@value.inspect} parent=#{@parent.inspect} children=#{@children.inspect}>"
  end
  
  # Safe implementation with recursion detection
  def inspect
    return "#<LinkedNode:#{object_id} value=#{@value.inspect} (inspecting...)>" if @inspecting
    
    @inspecting = true
    result = "#<LinkedNode:#{object_id} value=#{@value.inspect} parent=#{@parent ? @parent.object_id : 'nil'} children=#{@children.length}>"
    @inspecting = false
    result
  rescue
    @inspecting = false
    "#<LinkedNode:#{object_id} (inspection failed)>"
  end
end

# Create circular reference
parent = LinkedNode.new("parent")
child = LinkedNode.new("child")
parent.children << child
child.parent = parent

parent.inspect  # Safe: shows object_id instead of full parent inspection

Performance issues arise when inspection methods perform expensive operations or process large data sets. Developers often overlook the performance implications of complex inspection logic, especially in logging and debugging scenarios.

class ExpensiveInspection
  def initialize(data)
    @data = data
  end
  
  # Problematic: expensive operation in inspect
  def inspect_slow
    processed_data = @data.map { |item| expensive_calculation(item) }
    "#<ExpensiveInspection processed=#{processed_data.inspect}>"
  end
  
  # Better: defer expensive operations
  def inspect
    "#<ExpensiveInspection data_size=#{@data.size} sample=#{@data.first(3).inspect}>"
  end
  
  def detailed_inspect
    # Expensive version available when explicitly requested
    processed_data = @data.map { |item| expensive_calculation(item) }
    "#<ExpensiveInspection processed=#{processed_data.inspect}>"
  end
  
  private
  
  def expensive_calculation(item)
    # Simulate expensive operation
    sleep(0.01)
    item * 2
  end
end

Memory leaks can occur when inspection methods retain references to large objects or create unnecessary object allocations. This particularly affects long-running applications with frequent debugging output.

class MemoryEfficientInspection
  def initialize(large_dataset)
    @large_dataset = large_dataset
  end
  
  # Memory-inefficient: creates large string allocations
  def inspect_wasteful
    "#<DataProcessor dataset=#{@large_dataset.inspect}>"
  end
  
  # Memory-efficient: provides summary information
  def inspect
    "#<DataProcessor dataset_size=#{@large_dataset.size} memory=#{@large_dataset.size * 8} bytes>"
  end
end

Reference

Core Inspection Methods

Method Parameters Returns Description
#inspect None String Returns debugging representation of object
#to_s None String Returns string representation for display
#p(*objs) Zero or more objects Last object Prints inspect output and returns object
#pp(obj, out=$>, width=79) Object, output stream, width nil Pretty prints object with formatting
#display(port=$>) Output stream nil Writes string representation to stream

Object Identity Methods

Method Parameters Returns Description
#object_id None Integer Returns unique identifier for object
#class None Class Returns class of object
#instance_variables None Array<Symbol> Returns array of instance variable names
#instance_variable_get(symbol) Symbol Object Returns value of instance variable

Debugging Helper Methods

Method Parameters Returns Description
#method(name) Symbol or String Method Returns Method object for given name
#methods(regular=true) Boolean Array<Symbol> Returns array of method names
#respond_to?(method, private=false) Symbol, Boolean Boolean Tests if object responds to method
#kind_of?(class) Class Boolean Tests if object is instance of class

Pretty Print Options

Option Type Default Description
:width Integer 79 Maximum line width for formatting
:newline String "\n" Line separator string
:genspace Proc `lambda{ n
:output IO $> Output stream for printing

Common Escape Sequences in Inspect Output

Character Inspect Output Description
\n \\n Newline character
\t \\t Tab character
\" \\\" Double quote
\\ \\\\ Backslash
\r \\r Carriage return

Standard Object Inspection Formats

Object Type Inspect Format Example
String "content with \"quotes\""
Symbol :symbol_name
Array [1, "two", :three]
Hash {:key => "value", "string_key" => 42}
Range 1..10 or 1...10
Regexp /pattern/flags
Class #<Class:0x007f8b1c0a1234>
Module #<Module:0x007f8b1c0a1234>

Error Types in Inspection

Exception Common Cause Prevention Strategy
SystemStackError Circular references in inspect Implement recursion detection
NoMethodError Missing to_s or inspect method Define default implementations
ArgumentError Invalid parameters to pp or display Validate parameters before calling
Encoding::CompatibilityError Mixed encodings in string output Ensure consistent encoding
StandardError General exceptions in custom inspect Use rescue blocks in custom methods

Thread Safety Considerations

Scenario Risk Level Mitigation
Shared mutable state in inspect High Use local variables or synchronization
Global variables in inspection Medium Avoid or protect with mutexes
Instance variable modification Low Make inspection methods read-only
Concurrent pp output Low Use separate output streams