💎

【Ruby 35日目】基本文法 - メソッドチェーン

に公開

はじめに

Rubyのメソッドチェーンについて、Ruby 3.4の仕様に基づいて詳しく解説します。

この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。

基本概念

メソッドチェーンは、メソッドの戻り値に対して連続してメソッドを呼び出す手法です:

  • 可読性の向上 - 一連の処理を簡潔に表現できる
  • Fluent Interface - 流れるようなインターフェース設計
  • selfの返却 - メソッドがselfを返すことでチェーン可能に
  • 関数型プログラミング - map、select、reduceなどの組み合わせ

Rubyでは多くの組み込みメソッドがメソッドチェーンに対応しており、簡潔で読みやすいコードを書けます。

基本的な使い方

配列のメソッドチェーン

# 基本的なメソッドチェーン
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

result = numbers
  .select { |n| n.even? }
  .map { |n| n * 2 }
  .reduce(:+)

puts result  #=> 60 (2*2 + 4*2 + 6*2 + 8*2 + 10*2)

文字列のメソッドチェーン

text = "  hello world  "

result = text
  .strip
  .upcase
  .gsub("WORLD", "RUBY")
  .split
  .join("-")

puts result  #=> HELLO-RUBY

ハッシュのメソッドチェーン

data = { a: 1, b: 2, c: 3, d: 4 }

result = data
  .select { |k, v| v.even? }
  .transform_values { |v| v * 10 }
  .sort_by { |k, v| v }
  .to_h

puts result.inspect  #=> {:b=>20, :d=>40}

selfを返すメソッド

class Person
  attr_reader :name, :age, :country

  def initialize
    @name = ""
    @age = 0
    @country = ""
  end

  def set_name(name)
    @name = name
    self  # selfを返す
  end

  def set_age(age)
    @age = age
    self
  end

  def set_country(country)
    @country = country
    self
  end

  def display
    puts "#{@name} (#{@age}) from #{@country}"
    self
  end
end

person = Person.new
  .set_name("Alice")
  .set_age(25)
  .set_country("Japan")
  .display
#=> Alice (25) from Japan

thenメソッド(Ruby 2.6+)

# thenメソッドを使った処理のチェーン
result = "hello"
  .then { |s| s.upcase }
  .then { |s| s.reverse }
  .then { |s| s + "!" }

puts result  #=> !OLLEH

safe navigation operator(&.)

# nilの可能性がある場合の安全なチェーン
user = { name: "Alice", address: nil }

# 従来の書き方
city = user[:address] && user[:address][:city] && user[:address][:city].upcase

# safe navigation operator
city = user[:address]&.[](:city)&.upcase
puts city.inspect  #=> nil

# 値がある場合
user = { name: "Alice", address: { city: "Tokyo" } }
city = user[:address]&.[](:city)&.upcase
puts city  #=> TOKYO

よくあるユースケース

ケース1: データ変換パイプライン

複数のステップでデータを変換します。

class DataPipeline
  def initialize(data)
    @data = data
  end

  def filter(&block)
    @data = @data.select(&block)
    self
  end

  def transform(&block)
    @data = @data.map(&block)
    self
  end

  def sort_by_key(key)
    @data = @data.sort_by { |item| item[key] }
    self
  end

  def limit(n)
    @data = @data.first(n)
    self
  end

  def result
    @data
  end
end

users = [
  { name: "Alice", age: 30, score: 85 },
  { name: "Bob", age: 25, score: 92 },
  { name: "Charlie", age: 35, score: 78 },
  { name: "David", age: 28, score: 95 }
]

top_performers = DataPipeline.new(users)
  .filter { |user| user[:score] > 80 }
  .sort_by_key(:score)
  .limit(2)
  .transform { |user| "#{user[:name]}: #{user[:score]}" }
  .result

puts top_performers.inspect
#=> ["Alice: 85", "Bob: 92"]

ケース2: クエリビルダー

SQLクエリを段階的に構築します。

class QueryBuilder
  def initialize(table)
    @table = table
    @conditions = []
    @order = nil
    @limit = nil
  end

  def where(condition)
    @conditions << condition
    self
  end

  def order_by(column, direction = :asc)
    @order = "ORDER BY #{column} #{direction.to_s.upcase}"
    self
  end

  def limit(n)
    @limit = "LIMIT #{n}"
    self
  end

  def to_sql
    sql = "SELECT * FROM #{@table}"
    sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
    sql += " #{@order}" if @order
    sql += " #{@limit}" if @limit
    sql
  end
end

query = QueryBuilder.new("users")
  .where("age > 18")
  .where("country = 'Japan'")
  .order_by(:created_at, :desc)
  .limit(10)
  .to_sql

puts query
#=> SELECT * FROM users WHERE age > 18 AND country = 'Japan' ORDER BY created_at DESC LIMIT 10

ケース3: ビルダーパターン

複雑なオブジェクトを段階的に構築します。

class EmailBuilder
  def initialize
    @to = []
    @cc = []
    @subject = ""
    @body = ""
    @attachments = []
  end

  def to(*recipients)
    @to.concat(recipients)
    self
  end

  def cc(*recipients)
    @cc.concat(recipients)
    self
  end

  def subject(text)
    @subject = text
    self
  end

  def body(text)
    @body = text
    self
  end

  def attach(filename)
    @attachments << filename
    self
  end

  def send
    puts "Sending email..."
    puts "To: #{@to.join(', ')}"
    puts "CC: #{@cc.join(', ')}" unless @cc.empty?
    puts "Subject: #{@subject}"
    puts "Body: #{@body}"
    puts "Attachments: #{@attachments.join(', ')}" unless @attachments.empty?
    self
  end
end

EmailBuilder.new
  .to("alice@example.com", "bob@example.com")
  .cc("manager@example.com")
  .subject("Monthly Report")
  .body("Please find the attached report.")
  .attach("report.pdf")
  .attach("data.csv")
  .send
#=> Sending email...
#   To: alice@example.com, bob@example.com
#   CC: manager@example.com
#   Subject: Monthly Report
#   Body: Please find the attached report.
#   Attachments: report.pdf, data.csv

ケース4: テキスト処理チェーン

複数のテキスト処理を連続して適用します。

class TextProcessor
  def initialize(text)
    @text = text
  end

  def remove_whitespace
    @text = @text.gsub(/\s+/, " ").strip
    self
  end

  def capitalize_words
    @text = @text.split.map(&:capitalize).join(" ")
    self
  end

  def remove_special_chars
    @text = @text.gsub(/[^a-zA-Z0-9\s]/, "")
    self
  end

  def truncate(length, suffix = "...")
    if @text.length > length
      @text = @text[0...length] + suffix
    end
    self
  end

  def result
    @text
  end
end

input = "  hello,  WORLD!!!  this is    ruby.  "

output = TextProcessor.new(input)
  .remove_whitespace
  .remove_special_chars
  .capitalize_words
  .truncate(20)
  .result

puts output  #=> Hello World This...

ケース5: 設定オブジェクト

設定を流れるように記述します。

class Configuration
  attr_reader :settings

  def initialize
    @settings = {}
  end

  def set(key, value)
    @settings[key] = value
    self
  end

  def enable(feature)
    @settings[feature] = true
    self
  end

  def disable(feature)
    @settings[feature] = false
    self
  end

  def merge(hash)
    @settings.merge!(hash)
    self
  end

  def validate
    required = [:host, :port]
    missing = required - @settings.keys

    unless missing.empty?
      raise "Missing required settings: #{missing.join(', ')}"
    end

    self
  end

  def apply
    puts "Applying configuration:"
    @settings.each { |k, v| puts "  #{k}: #{v}" }
    self
  end
end

config = Configuration.new
  .set(:host, "localhost")
  .set(:port, 3000)
  .enable(:debug)
  .enable(:cache)
  .disable(:verbose)
  .merge({ timeout: 30, retry: 3 })
  .validate
  .apply
#=> Applying configuration:
#     host: localhost
#     port: 3000
#     debug: true
#     cache: true
#     verbose: false
#     timeout: 30
#     retry: 3

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

注意点

  1. nilを返すメソッドに注意
# BAD: nilを返すメソッドをチェーンすると NoMethodError
arr = [1, 2, 3]
# result = arr.find { |n| n > 5 }.to_s.upcase  # NoMethodError

# GOOD: safe navigation operatorを使う
result = arr.find { |n| n > 5 }&.to_s&.upcase
puts result.inspect  #=> nil

# GOOD: デフォルト値を用意
result = (arr.find { |n| n > 5 } || 0).to_s.upcase
puts result  #=> 0
  1. 破壊的メソッドとチェーン
# 破壊的メソッドは元のオブジェクトを変更してselfを返す
text = "hello"
result = text.upcase!.reverse!
puts result  #=> OLLEH
puts text    #=> OLLEH(元のオブジェクトも変更されている)

# 非破壊的メソッドは新しいオブジェクトを返す
text = "hello"
result = text.upcase.reverse
puts result  #=> OLLEH
puts text    #=> hello(元のオブジェクトは変更されていない)
  1. 長すぎるチェーンは可読性を損なう
# BAD: 長すぎて読みにくい
result = data.select { |x| x > 0 }.map { |x| x * 2 }.group_by { |x| x % 3 }.transform_values { |v| v.sum }.select { |k, v| v > 10 }.sort_by { |k, v| v }.reverse.to_h

# GOOD: 適切に改行して読みやすく
result = data
  .select { |x| x > 0 }
  .map { |x| x * 2 }
  .group_by { |x| x % 3 }
  .transform_values { |v| v.sum }
  .select { |k, v| v > 10 }
  .sort_by { |k, v| v }
  .reverse
  .to_h

ベストプラクティス

  1. 適切な改行で可読性を保つ
# GOOD: メソッドチェーンは適切に改行
users = User.all
  .where(active: true)
  .order(created_at: :desc)
  .limit(10)
  .map { |user| user.to_json }
  1. selfを返すメソッドを設計
# GOOD: メソッドチェーン可能なクラス設計
class Builder
  def initialize
    @data = {}
  end

  def add(key, value)
    @data[key] = value
    self  # selfを返す
  end

  def remove(key)
    @data.delete(key)
    self  # selfを返す
  end

  def build
    @data  # 最後は結果を返す
  end
end

result = Builder.new
  .add(:a, 1)
  .add(:b, 2)
  .remove(:a)
  .build

puts result.inspect  #=> {:b=>2}
  1. thenメソッドで複雑な処理を表現
# GOOD: thenで明示的な変換
result = input
  .then { |s| validate(s) }
  .then { |s| normalize(s) }
  .then { |s| process(s) }

def validate(s)
  raise "Invalid input" if s.empty?
  s
end

def normalize(s)
  s.strip.downcase
end

def process(s)
  s.gsub(/\s+/, "_")
end

puts result

Ruby 3.4での改善点

  • Prismパーサーによる最適化 - メソッドチェーンの解析が高速化
  • YJITの最適化 - チェーンされたメソッド呼び出しのインライン化が改善
  • itパラメータとの組み合わせ - より簡潔なチェーン記述が可能
  • パフォーマンス向上 - 長いメソッドチェーンの実行速度が向上
# Ruby 3.4の新機能:itパラメータを使った簡潔な記述
numbers = [1, 2, 3, 4, 5]

# 従来の書き方
result = numbers.select { |n| n.even? }.map { |n| n * 2 }

# itを使った書き方(Ruby 3.4+)
result = numbers.select { it.even? }.map { it * 2 }
puts result.inspect  #=> [4, 8]

まとめ

この記事では、メソッドチェーンについて以下の内容を学びました:

  • 基本概念と重要性 - 可読性の向上、Fluent Interface、selfの返却
  • 基本的な使い方と構文 - 配列、文字列、ハッシュのチェーン、thenメソッド
  • 実践的なユースケース - データパイプライン、クエリビルダー、ビルダーパターン、テキスト処理、設定管理
  • 注意点とベストプラクティス - nilの扱い、改行による可読性、selfを返す設計

メソッドチェーンを適切に使用することで、簡潔で読みやすいコードを書くことができます。

参考資料

Discussion