💎
【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
注意点とベストプラクティス
注意点
- 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
- 破壊的メソッドとチェーン
# 破壊的メソッドは元のオブジェクトを変更して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(元のオブジェクトは変更されていない)
- 長すぎるチェーンは可読性を損なう
# 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
ベストプラクティス
- 適切な改行で可読性を保つ
# GOOD: メソッドチェーンは適切に改行
users = User.all
.where(active: true)
.order(created_at: :desc)
.limit(10)
.map { |user| user.to_json }
- 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}
- 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