CrackedRuby logo

CrackedRuby

Method Queries

Method queries provide boolean-returning methods that check object state, properties, and relationships across Ruby's type system.

Metaprogramming Reflection and Introspection
5.1.3

Overview

Method queries form a consistent pattern across Ruby's built-in classes and standard library. These methods return boolean values to check object states, collection properties, type relationships, and capability queries. Ruby implements method queries through a naming convention where predicate methods end with a question mark, making their boolean nature immediately apparent in code.

The query method pattern appears throughout Ruby's core classes. String objects provide queries like empty? and start_with?. Array and Hash objects implement include? and key? methods. Object itself provides fundamental queries such as nil?, is_a?, and respond_to?. This consistency creates a uniform interface for boolean checks across different data types.

string = "hello"
string.empty?     # => false
string.include?("e") # => true

array = [1, 2, 3]
array.empty?      # => false
array.include?(2) # => true

hash = { name: "Ruby", year: 1995 }
hash.empty?       # => false
hash.key?(:name)  # => true

Ruby's method query system extends beyond simple state checks. Type queries like is_a? and kind_of? check inheritance relationships. Capability queries like respond_to? test method availability. State queries such as frozen? and tainted? examine object mutability and security flags.

The method query pattern integrates with Ruby's conditional expressions and boolean operators. Query results work directly in if statements, unless blocks, and compound boolean expressions. This design makes method queries the primary mechanism for state-based conditional logic in Ruby programs.

Basic Usage

Method queries replace explicit comparisons with expressive boolean methods. Rather than comparing values to constants or checking collection lengths, query methods provide direct boolean results. This approach reduces cognitive overhead and improves code readability.

String queries check content and format properties. The empty? method returns true for zero-length strings. The include? method searches for substring presence. Format queries like start_with? and end_with? check string boundaries. Pattern queries such as match? test regular expression matches.

text = "Ruby programming"

# Content queries
text.empty?                    # => false
text.include?("Ruby")          # => true
text.include?("Python")        # => false

# Format queries
text.start_with?("Ruby")       # => true
text.end_with?("programming")  # => true
text.start_with?("Java")       # => false

# Pattern queries
text.match?(/\w+/)            # => true
text.match?(/\d+/)            # => false

Collection queries examine array and hash contents. The empty? method checks for zero elements. The include? method tests element presence in arrays. Hash-specific queries like key? and value? check for key or value existence. The has_key? method provides an alias for key?.

numbers = [1, 2, 3, 4, 5]
empty_array = []

# Array queries
numbers.empty?        # => false
empty_array.empty?    # => true
numbers.include?(3)   # => true
numbers.include?(10)  # => false

person = { name: "Alice", age: 30, city: "New York" }
empty_hash = {}

# Hash queries
person.empty?           # => false
empty_hash.empty?       # => true
person.key?(:name)      # => true
person.key?(:email)     # => false
person.value?("Alice")  # => true
person.value?("Bob")    # => false

Type and capability queries determine object relationships and available methods. The is_a? method checks class inheritance, returning true if the object belongs to the specified class or inherits from it. The kind_of? method provides identical functionality as an alias. The instance_of? method checks exact class membership without inheritance. The respond_to? method tests method availability.

string = "hello"
number = 42
array = [1, 2, 3]

# Type queries
string.is_a?(String)    # => true
string.is_a?(Object)    # => true
string.is_a?(Integer)   # => false

number.kind_of?(Integer) # => true
number.kind_of?(Numeric) # => true
number.instance_of?(Integer) # => true
number.instance_of?(Numeric) # => false

# Capability queries
string.respond_to?(:upcase)  # => true
string.respond_to?(:push)    # => false
array.respond_to?(:push)     # => true
array.respond_to?(:upcase)   # => false

State queries examine object properties and flags. The nil? method checks for nil values. The frozen? method determines object mutability. Numeric queries like zero?, positive?, and negative? test mathematical properties. The even? and odd? methods check integer parity.

value = nil
name = "Ruby"
temperature = -5

# Nil checks
value.nil?  # => true
name.nil?   # => false

# State checks
name.frozen?  # => false
name.freeze
name.frozen?  # => true

# Numeric queries
temperature.zero?      # => false
temperature.negative?  # => true
temperature.positive?  # => false

number = 8
number.even?  # => true
number.odd?   # => false

Advanced Usage

Custom method queries extend the predicate pattern to domain-specific classes. Ruby classes define query methods using the question mark suffix convention. These methods implement business logic as boolean expressions, encapsulating complex conditions within descriptive method names. Custom queries improve code readability by replacing complex boolean expressions with intention-revealing method calls.

class BankAccount
  def initialize(balance, overdraft_limit = 0)
    @balance = balance
    @overdraft_limit = overdraft_limit
    @frozen = false
  end
  
  def overdrawn?
    @balance < 0
  end
  
  def can_withdraw?(amount)
    (@balance - amount) >= -@overdraft_limit && !@frozen
  end
  
  def frozen?
    @frozen
  end
  
  def has_overdraft_protection?
    @overdraft_limit > 0
  end
  
  def balance_above?(threshold)
    @balance > threshold
  end
  
  def freeze!
    @frozen = true
  end
end

account = BankAccount.new(1000, 500)

account.overdrawn?                 # => false
account.can_withdraw?(1200)        # => true
account.can_withdraw?(1600)        # => false
account.has_overdraft_protection?  # => true
account.balance_above?(500)        # => true

account.freeze!
account.frozen?                    # => true
account.can_withdraw?(100)         # => false

Metaprogramming techniques generate query methods dynamically. The define_method approach creates multiple query methods from configuration data. This pattern reduces code duplication when implementing similar query logic across multiple attributes or states.

class Document
  STATUSES = [:draft, :review, :approved, :published, :archived]
  
  def initialize(status = :draft)
    @status = status
    @metadata = {}
  end
  
  # Generate status query methods
  STATUSES.each do |status|
    define_method "#{status}?" do
      @status == status
    end
  end
  
  # Generate metadata presence queries
  def self.define_metadata_query(field)
    define_method "has_#{field}?" do
      !@metadata[field].nil? && !@metadata[field].empty?
    end
  end
  
  define_metadata_query :title
  define_metadata_query :author
  define_metadata_query :tags
  
  def set_metadata(field, value)
    @metadata[field] = value
  end
  
  def change_status(new_status)
    @status = new_status if STATUSES.include?(new_status)
  end
end

doc = Document.new
doc.draft?      # => true
doc.published?  # => false

doc.set_metadata(:title, "Ruby Guide")
doc.set_metadata(:author, "Developer")
doc.has_title?   # => true
doc.has_author?  # => true
doc.has_tags?    # => false

doc.change_status(:review)
doc.draft?   # => false
doc.review?  # => true

Query method chaining combines multiple boolean checks using logical operators. Ruby's short-circuit evaluation optimizes chained queries by stopping evaluation when results become deterministic. Complex business rules compose from simpler query methods, maintaining readability while implementing sophisticated conditional logic.

class User
  def initialize(name, email, age, verified = false)
    @name = name
    @email = email
    @age = age
    @verified = verified
    @last_login = nil
  end
  
  def adult?
    @age >= 18
  end
  
  def verified?
    @verified
  end
  
  def has_email?
    !@email.nil? && !@email.empty?
  end
  
  def recent_login?
    @last_login && (@last_login > (Time.now - 86400))
  end
  
  def can_purchase_alcohol?
    adult? && verified? && recent_login?
  end
  
  def eligible_for_newsletter?
    has_email? && verified?
  end
  
  def requires_verification?
    !verified? && adult?
  end
  
  def login!
    @last_login = Time.now
  end
  
  def verify!
    @verified = true
  end
end

user = User.new("Alice", "alice@example.com", 25)

user.adult?                    # => true
user.verified?                 # => false
user.recent_login?             # => false
user.can_purchase_alcohol?     # => false
user.requires_verification?    # => true

user.verify!
user.login!
user.can_purchase_alcohol?     # => true
user.eligible_for_newsletter?  # => true

Module mixins share query method functionality across multiple classes. Modules define reusable query methods that classes include through mixins. This pattern promotes code reuse while maintaining the query method convention across different class hierarchies.

module Timestampable
  def created_today?
    created_at.to_date == Date.today
  end
  
  def created_this_week?
    created_at > 1.week.ago
  end
  
  def created_this_month?
    created_at > 1.month.ago
  end
  
  def stale?
    created_at < 1.year.ago
  end
end

module Taggable
  def tagged?
    tags.any?
  end
  
  def tagged_with?(tag_name)
    tags.include?(tag_name.to_s)
  end
  
  def has_multiple_tags?
    tags.length > 1
  end
end

class BlogPost
  include Timestampable
  include Taggable
  
  attr_reader :title, :created_at, :tags
  
  def initialize(title, tags = [])
    @title = title
    @created_at = Time.now
    @tags = tags.map(&:to_s)
  end
  
  def published?
    !@title.nil? && !@title.empty?
  end
end

class Comment
  include Timestampable
  
  attr_reader :body, :created_at
  
  def initialize(body)
    @body = body
    @created_at = Time.now
  end
  
  def empty?
    @body.nil? || @body.strip.empty?
  end
end

post = BlogPost.new("Ruby Queries", ["programming", "ruby"])
post.published?         # => true
post.created_today?     # => true
post.tagged?           # => true
post.tagged_with?("ruby") # => true

comment = Comment.new("Great post!")
comment.created_today?  # => true
comment.empty?         # => false

Common Pitfalls

Method queries return various truthy and falsy values beyond strict boolean types. Ruby treats nil and false as falsy, while all other values evaluate as truthy in conditional contexts. However, query methods may return objects other than true or false, creating subtle bugs when code expects strict boolean values.

The String#match method returns a MatchData object or nil, not true or false. Code comparing the result to true fails unexpectedly. The String#match? method provides boolean results for conditional logic. Similarly, Array#index returns an integer or nil, while Array#include? returns true or false.

text = "Ruby programming"

# Problematic: match returns MatchData or nil
result = text.match(/Ruby/)
if result == true  # => false, even though match succeeds
  puts "Found Ruby"
end

# Better: use match? for boolean results
if text.match?(/Ruby/)  # => true
  puts "Found Ruby"
end

# Problematic: index returns integer or nil
numbers = [1, 2, 3]
index = numbers.index(2)
if index == true  # => false, index is 1
  puts "Found number"
end

# Better: use include? for boolean results
if numbers.include?(2)  # => true
  puts "Found number"
end

# Conditional usage works correctly
if text.match(/Ruby/)     # => truthy (MatchData object)
  puts "Match found"
end

if numbers.index(2)       # => truthy (integer 1)
  puts "Index found"
end

Custom query methods must return appropriate boolean values for their intended usage. Methods returning nil, empty strings, or zero may behave unexpectedly in boolean contexts. Explicit boolean conversion using double negation or comparison operators ensures consistent conditional behavior.

class Product
  def initialize(name, price, stock)
    @name = name
    @price = price
    @stock = stock
  end
  
  # Problematic: returns stock quantity
  def in_stock_bad?
    @stock
  end
  
  # Better: returns explicit boolean
  def in_stock?
    @stock > 0
  end
  
  # Problematic: returns price or nil
  def expensive_bad?
    @price if @price > 100
  end
  
  # Better: returns boolean
  def expensive?
    @price > 100
  end
  
  # Acceptable: truthy/falsy usage
  def has_name_truthy?
    @name && !@name.empty?
  end
  
  # Explicit boolean conversion
  def has_name_boolean?
    !!(@name && !@name.empty?)
  end
end

product = Product.new("Laptop", 150, 0)

# These work in conditionals but may surprise
if product.in_stock_bad?  # => false (0 is falsy)
  puts "Available"
end

if product.expensive_bad?  # => true (150 is truthy)
  puts "Expensive"
end

# These provide clear boolean semantics
product.in_stock?      # => false
product.expensive?     # => true
product.has_name_boolean?  # => true

Type checking queries exhibit inheritance behavior that may surprise developers. The is_a? and kind_of? methods return true for ancestor classes, while instance_of? checks exact class membership. Code assuming exact type matches may fail with inherited objects.

class Animal
  def speak
    "Some sound"
  end
end

class Dog < Animal
  def speak
    "Woof"
  end
end

dog = Dog.new

# Inheritance-aware queries
dog.is_a?(Dog)     # => true
dog.is_a?(Animal)  # => true
dog.is_a?(Object)  # => true
dog.kind_of?(Animal)  # => true

# Exact class query
dog.instance_of?(Dog)     # => true
dog.instance_of?(Animal)  # => false

# Problematic: assuming exact type
def process_animal(obj)
  if obj.is_a?(Animal)
    # This catches Dog objects too
    puts "Processing generic animal"
  end
end

# Better: check specific types first
def process_animal_better(obj)
  case obj
  when Dog
    puts "Processing dog specifically"
  when Animal
    puts "Processing generic animal"
  end
end

Query method naming conflicts arise when classes define methods matching Ruby's built-in query methods. Custom empty? methods may not behave consistently with collection empty? semantics. The respond_to? method may return false positives when classes define method_missing.

class CustomCollection
  def initialize
    @items = []
    @hidden_items = ["secret"]
  end
  
  # Conflicts with standard empty? semantics
  def empty?
    @items.empty? && @hidden_items.empty?
  end
  
  # Standard collection methods
  def <<(item)
    @items << item
  end
  
  def size
    @items.size
  end
  
  # Problematic method_missing behavior
  def method_missing(name, *args)
    if name.to_s.end_with?('?')
      # All query methods return true
      true
    else
      super
    end
  end
end

collection = CustomCollection.new
collection << "item"

# Unexpected behavior: reports empty despite having items
collection.empty?  # => false (because hidden_items not empty)
collection.size    # => 1

# respond_to? shows false positives
collection.respond_to?(:nonexistent_query?)  # => true (via method_missing)
collection.nonexistent_query?                # => true

# Better: explicit query method definitions
class BetterCollection
  def initialize
    @items = []
  end
  
  def empty?
    @items.empty?
  end
  
  def has_items?
    !@items.empty?
  end
  
  def respond_to_missing?(name, include_private = false)
    name.to_s.end_with?('?') || super
  end
  
  def method_missing(name, *args)
    if name.to_s.end_with?('?')
      false  # Explicit false for unknown queries
    else
      super
    end
  end
end

Performance assumptions about query methods may lead to inefficient code. Some query methods perform expensive operations, particularly those involving iteration or complex calculations. Caching query results or restructuring algorithms may improve performance for frequently called queries.

class LargeDataset
  def initialize(data)
    @data = data
  end
  
  # Expensive: iterates through entire dataset
  def has_duplicates?
    @data.uniq.length != @data.length
  end
  
  # Expensive: multiple iterations
  def all_positive?
    @data.all? { |n| n > 0 }
  end
  
  def any_negative?
    @data.any? { |n| n < 0 }
  end
  
  # Better: cached results
  def has_duplicates_cached?
    @duplicates_checked ||= begin
      @data.uniq.length != @data.length
    end
  end
  
  # Better: early termination
  def mixed_signs?
    has_positive = false
    has_negative = false
    
    @data.each do |n|
      has_positive = true if n > 0
      has_negative = true if n < 0
      return true if has_positive && has_negative
    end
    
    false
  end
end

# Performance test scenario
large_data = Array.new(100_000) { rand(-1000..1000) }
dataset = LargeDataset.new(large_data)

# Multiple calls show caching benefit
5.times { dataset.has_duplicates_cached? }  # First call expensive, rest cached

Reference

Core Object Queries

Method Parameters Returns Description
#nil? none Boolean Returns true if object is nil
#is_a?(class) class (Class/Module) Boolean Returns true if object is instance of class or inherits from it
#kind_of?(class) class (Class/Module) Boolean Alias for is_a?
#instance_of?(class) class (Class) Boolean Returns true if object is direct instance of class
#respond_to?(method, include_private = false) method (String/Symbol), include_private (Boolean) Boolean Returns true if object responds to method
#frozen? none Boolean Returns true if object is frozen

String Queries

Method Parameters Returns Description
#empty? none Boolean Returns true if string has zero length
#include?(substring) substring (String) Boolean Returns true if string contains substring
#start_with?(*prefixes) prefixes (String) Boolean Returns true if string starts with any prefix
#end_with?(*suffixes) suffixes (String) Boolean Returns true if string ends with any suffix
#match?(pattern, pos = 0) pattern (Regexp/String), pos (Integer) Boolean Returns true if pattern matches string
#ascii_only? none Boolean Returns true if string contains only ASCII characters

Array Queries

Method Parameters Returns Description
#empty? none Boolean Returns true if array has zero elements
#include?(element) element (Object) Boolean Returns true if array contains element
#any?(&block) block (Proc) Boolean Returns true if any element matches block condition
#all?(&block) block (Proc) Boolean Returns true if all elements match block condition
#one?(&block) block (Proc) Boolean Returns true if exactly one element matches block
#none?(&block) block (Proc) Boolean Returns true if no elements match block condition

Hash Queries

Method Parameters Returns Description
#empty? none Boolean Returns true if hash has zero key-value pairs
#key?(key) key (Object) Boolean Returns true if hash contains key
#has_key?(key) key (Object) Boolean Alias for key?
#value?(value) value (Object) Boolean Returns true if hash contains value
#has_value?(value) value (Object) Boolean Alias for value?
#include?(key) key (Object) Boolean Alias for key?

Numeric Queries

Method Parameters Returns Description
#zero? none Boolean Returns true if number equals zero
#positive? none Boolean Returns true if number is greater than zero
#negative? none Boolean Returns true if number is less than zero
#even? none Boolean Returns true if integer is even (Integer only)
#odd? none Boolean Returns true if integer is odd (Integer only)
#finite? none Boolean Returns true if float is finite (Float only)
#infinite? none Integer/nil Returns 1, -1 for positive/negative infinity, nil otherwise
#nan? none Boolean Returns true if float is NaN (Float only)

File and IO Queries

Method Parameters Returns Description
File.exist?(path) path (String) Boolean Returns true if file or directory exists
File.file?(path) path (String) Boolean Returns true if path is regular file
File.directory?(path) path (String) Boolean Returns true if path is directory
File.readable?(path) path (String) Boolean Returns true if file is readable
File.writable?(path) path (String) Boolean Returns true if file is writable
File.executable?(path) path (String) Boolean Returns true if file is executable
#eof? none Boolean Returns true if IO stream is at end
#closed? none Boolean Returns true if IO stream is closed

Query Method Patterns

Boolean Return Values:

  • true: Condition satisfied, property present, test passed
  • false: Condition not satisfied, property absent, test failed

Naming Conventions:

  • End with question mark (?) for predicate methods
  • Use descriptive names indicating what is being tested
  • Prefer positive assertions (present?) over negative ones (not_empty?)

Performance Considerations:

  • Simple queries (property checks): O(1) complexity
  • Collection queries with blocks: O(n) complexity
  • File system queries: Variable based on OS and storage
  • Type queries: O(1) complexity for class hierarchy checks

Common Query Patterns:

  • State queries: empty?, frozen?, closed?
  • Content queries: include?, key?, value?
  • Type queries: is_a?, instance_of?, respond_to?
  • Format queries: start_with?, end_with?, match?
  • Capability queries: readable?, writable?, executable?