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 passedfalse
: 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?