💎
【Ruby 36日目】基本文法 - 述語メソッド
はじめに
Rubyの述語メソッド(Predicate Methods)について、Ruby 3.4の仕様に基づいて詳しく解説します。
この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。
基本概念
述語メソッドは、真偽値(true/false)を返すメソッドで、?で終わる命名規則があります:
-
命名規則 - メソッド名が
?で終わる - 戻り値 - true または false を返す
- 可読性 - コードの意図が明確になる
-
組み込みメソッド -
empty?、nil?、include?など多数
述語メソッドを使うことで、条件判定の意図が明確になり、コードの可読性が向上します。
基本的な使い方
組み込みの述語メソッド
# nil?
value = nil
puts value.nil? #=> true
value = 10
puts value.nil? #=> false
# empty?
arr = []
puts arr.empty? #=> true
str = ""
puts str.empty? #=> true
# zero?
num = 0
puts num.zero? #=> true
# even? / odd?
puts 4.even? #=> true
puts 5.odd? #=> true
# include?
arr = [1, 2, 3, 4, 5]
puts arr.include?(3) #=> true
puts arr.include?(10) #=> false
# start_with? / end_with?
str = "hello world"
puts str.start_with?("hello") #=> true
puts str.end_with?("world") #=> true
カスタム述語メソッドの定義
class User
attr_reader :name, :age, :role
def initialize(name, age, role)
@name = name
@age = age
@role = role
end
def adult?
@age >= 18
end
def admin?
@role == :admin
end
def valid_name?
!@name.nil? && !@name.empty?
end
end
user = User.new("Alice", 25, :admin)
puts user.adult? #=> true
puts user.admin? #=> true
puts user.valid_name? #=> true
条件分岐での使用
numbers = [1, 2, 3, 4, 5]
if numbers.empty?
puts "Array is empty"
else
puts "Array has #{numbers.size} elements"
end
#=> Array has 5 elements
# ガード節として使用
def process(data)
return "No data" if data.nil? || data.empty?
data.map(&:to_s).join(", ")
end
puts process([1, 2, 3]) #=> 1, 2, 3
puts process([]) #=> No data
述語メソッドの組み合わせ
class Product
attr_reader :name, :price, :in_stock
def initialize(name, price, in_stock)
@name = name
@price = price
@in_stock = in_stock
end
def available?
in_stock && !price.nil? && price > 0
end
def expensive?
price && price > 10000
end
def affordable?
!expensive?
end
end
product = Product.new("Laptop", 150000, true)
puts product.available? #=> true
puts product.expensive? #=> true
puts product.affordable? #=> false
配列やハッシュでのフィルタリング
users = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 17, active: false },
{ name: "Charlie", age: 30, active: true }
]
# 述語メソッドを使ったフィルタリング
adults = users.select { |user| user[:age] >= 18 }
puts adults.inspect
#=> [{:name=>"Alice", :age=>25, :active=>true}, {:name=>"Charlie", :age=>30, :active=>true}]
active_users = users.select { |user| user[:active] }
puts active_users.inspect
#=> [{:name=>"Alice", :age=>25, :active=>true}, {:name=>"Charlie", :age=>30, :active=>true}]
# any? / all? / none?
puts users.any? { |user| user[:age] < 18 } #=> true
puts users.all? { |user| user[:active] } #=> false
puts users.none? { |user| user[:age] > 100 } #=> true
よくあるユースケース
ケース1: バリデーション
データの妥当性をチェックします。
class Email
attr_reader :address
def initialize(address)
@address = address
end
def valid?
valid_format? && valid_domain?
end
def valid_format?
return false if address.nil? || address.empty?
address.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
def valid_domain?
return false unless valid_format?
domain = address.split('@').last
!domain.nil? && domain.include?('.')
end
def gmail?
address&.end_with?('@gmail.com')
end
end
email1 = Email.new("user@example.com")
puts email1.valid? #=> true
puts email1.gmail? #=> false
email2 = Email.new("user@gmail.com")
puts email2.valid? #=> true
puts email2.gmail? #=> true
email3 = Email.new("invalid")
puts email3.valid? #=> false
ケース2: 状態管理
オブジェクトの状態を判定します。
class Order
attr_reader :status, :items, :paid
def initialize
@status = :pending
@items = []
@paid = false
end
def add_item(item)
@items << item
end
def confirm
@status = :confirmed if pending?
end
def ship
@status = :shipped if confirmed? && paid?
end
def pending?
@status == :pending
end
def confirmed?
@status == :confirmed
end
def shipped?
@status == :shipped
end
def paid?
@paid
end
def ready_to_ship?
confirmed? && paid? && !items.empty?
end
def mark_as_paid
@paid = true
end
end
order = Order.new
order.add_item("Book")
puts order.pending? #=> true
puts order.ready_to_ship? #=> false
order.confirm
order.mark_as_paid
puts order.confirmed? #=> true
puts order.ready_to_ship? #=> true
ケース3: 権限チェック
ユーザーの権限を判定します。
class User
attr_reader :role, :verified, :subscription
def initialize(role:, verified: false, subscription: :free)
@role = role
@verified = verified
@subscription = subscription
end
def admin?
role == :admin
end
def moderator?
role == :moderator
end
def premium?
subscription == :premium
end
def verified?
verified
end
def can_post?
verified?
end
def can_delete_post?(post)
admin? || (moderator? && post.flagged?)
end
def can_access_premium_content?
premium? || admin?
end
end
class Post
attr_reader :flagged
def initialize(flagged: false)
@flagged = flagged
end
def flagged?
@flagged
end
end
admin = User.new(role: :admin, verified: true, subscription: :free)
puts admin.can_access_premium_content? #=> true
user = User.new(role: :user, verified: true, subscription: :premium)
puts user.can_post? #=> true
puts user.can_access_premium_content? #=> true
post = Post.new(flagged: true)
moderator = User.new(role: :moderator, verified: true)
puts moderator.can_delete_post?(post) #=> true
ケース4: コレクションの検索
複雑な条件でデータを検索します。
class ProductRepository
def initialize(products)
@products = products
end
def available_products
@products.select(&:available?)
end
def out_of_stock_products
@products.reject(&:available?)
end
def has_available_products?
@products.any?(&:available?)
end
def all_in_stock?
@products.all?(&:available?)
end
def has_expensive_products?
@products.any?(&:expensive?)
end
def find_by_name(name)
@products.find { |p| p.name.downcase.include?(name.downcase) }
end
end
class SimpleProduct
attr_reader :name, :price, :stock
def initialize(name, price, stock)
@name = name
@price = price
@stock = stock
end
def available?
stock > 0
end
def expensive?
price > 10000
end
end
products = [
SimpleProduct.new("Laptop", 150000, 5),
SimpleProduct.new("Mouse", 2000, 0),
SimpleProduct.new("Keyboard", 8000, 10)
]
repo = ProductRepository.new(products)
puts repo.has_available_products? #=> true
puts repo.all_in_stock? #=> false
puts repo.has_expensive_products? #=> true
available = repo.available_products
puts available.map(&:name).inspect
#=> ["Laptop", "Keyboard"]
ケース5: 設定の検証
アプリケーション設定の妥当性をチェックします。
class Configuration
attr_reader :host, :port, :ssl, :timeout
def initialize(host: nil, port: nil, ssl: false, timeout: 30)
@host = host
@port = port
@ssl = ssl
@timeout = timeout
end
def valid?
has_host? && has_port? && valid_port? && valid_timeout?
end
def has_host?
!host.nil? && !host.empty?
end
def has_port?
!port.nil?
end
def valid_port?
return false unless has_port?
port.is_a?(Integer) && port > 0 && port <= 65535
end
def valid_timeout?
timeout.is_a?(Integer) && timeout > 0
end
def secure?
ssl
end
def local?
host == "localhost" || host == "127.0.0.1"
end
def development?
local? && !secure?
end
def production_ready?
valid? && secure? && !local?
end
end
config1 = Configuration.new(host: "localhost", port: 3000)
puts config1.valid? #=> true
puts config1.development? #=> true
puts config1.production_ready? #=> false
config2 = Configuration.new(host: "example.com", port: 443, ssl: true)
puts config2.valid? #=> true
puts config2.secure? #=> true
puts config2.production_ready? #=> true
注意点とベストプラクティス
注意点
- 述語メソッドは必ずtrue/falseを返す
# BAD: nilや他の値を返す
class User
def admin?
@role # :admin, :user, nilなどを返す可能性
end
end
# GOOD: true/falseを明示的に返す
class User
def admin?
@role == :admin
end
end
user = User.new
puts user.admin? ? "Admin" : "Not admin"
- 否定形の述語メソッドには注意
# 二重否定は避ける
class Task
def not_completed?
!completed?
end
def completed?
@completed
end
end
# BETTER: 肯定形で表現
class Task
def pending?
!completed?
end
def completed?
@completed
end
end
task = Task.new
puts task.pending? # より分かりやすい
- 述語メソッドに副作用を持たせない
# BAD: 述語メソッドで状態を変更
class Counter
def zero?
@count = 0 # 副作用
true
end
end
# GOOD: 述語メソッドは状態をチェックするだけ
class Counter
def zero?
@count == 0
end
def reset
@count = 0
end
end
ベストプラクティス
- 意味のある名前を付ける
# GOOD: 名前から意図が分かる
class User
def active?
@status == :active
end
def verified?
@verified_at.nil? == false
end
def can_post?
active? && verified?
end
end
- 複雑な条件は述語メソッドに抽出
# BAD: 条件が複雑で読みにくい
if user.age >= 18 && user.verified && (user.role == :admin || user.role == :moderator)
# ...
end
# GOOD: 述語メソッドで意図を明確に
class User
def can_moderate?
adult? && verified? && (admin? || moderator?)
end
def adult?
age >= 18
end
def verified?
@verified
end
def admin?
role == :admin
end
def moderator?
role == :moderator
end
end
if user.can_moderate?
# ...
end
- 組み込みメソッドを活用
# GOOD: 組み込みの述語メソッドを活用
arr = [1, 2, 3, 4, 5]
puts arr.any? { |n| n > 3 } #=> true
puts arr.all? { |n| n > 0 } #=> true
puts arr.none? { |n| n < 0 } #=> true
puts arr.one? { |n| n == 3 } #=> true
# String
str = "hello"
puts str.empty? #=> false
puts str.start_with?("he") #=> true
puts str.include?("ll") #=> true
Ruby 3.4での改善点
- Prismパーサーによる最適化 - 述語メソッドの解析が高速化
- YJITの最適化 - 述語メソッド呼び出しのパフォーマンスが向上
- パターンマッチングとの組み合わせ - 述語メソッドをガード節で使用可能
-
itパラメータとの組み合わせ - より簡潔な述語の記述が可能
# パターンマッチングでの使用
def process_user(user)
case user
in { role: :admin } if user.verified?
"Admin access granted"
in { role: :user } if user.verified?
"User access granted"
else
"Access denied"
end
end
# itパラメータを使った簡潔な記述(Ruby 3.4+)
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = numbers.select { it.even? }
puts even_numbers.inspect #=> [2, 4, 6]
まとめ
この記事では、述語メソッドについて以下の内容を学びました:
- 基本概念と重要性 -
?で終わる命名規則、true/falseの返却 - 基本的な使い方と構文 - 組み込みメソッド、カスタム述語メソッドの定義
- 実践的なユースケース - バリデーション、状態管理、権限チェック、検索、設定検証
- 注意点とベストプラクティス - true/falseの返却、副作用の回避、意味のある命名
述語メソッドを適切に使用することで、可読性が高く保守しやすいコードを書くことができます。
Discussion