💎

【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

注意点とベストプラクティス

注意点

  1. 述語メソッドは必ず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"
  1. 否定形の述語メソッドには注意
# 二重否定は避ける
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?  # より分かりやすい
  1. 述語メソッドに副作用を持たせない
# BAD: 述語メソッドで状態を変更
class Counter
  def zero?
    @count = 0  # 副作用
    true
  end
end

# GOOD: 述語メソッドは状態をチェックするだけ
class Counter
  def zero?
    @count == 0
  end

  def reset
    @count = 0
  end
end

ベストプラクティス

  1. 意味のある名前を付ける
# GOOD: 名前から意図が分かる
class User
  def active?
    @status == :active
  end

  def verified?
    @verified_at.nil? == false
  end

  def can_post?
    active? && verified?
  end
end
  1. 複雑な条件は述語メソッドに抽出
# 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
  1. 組み込みメソッドを活用
# 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