💎
【Ruby 26日目】基本文法 - イテレータの種類
はじめに
Rubyのイテレータの種類について、基本的なものから便利なバリエーションまで解説します。
イテレータはRubyの強力な機能の一つで、コレクションの要素を効率的に処理できます。適切なイテレータを選ぶことで、コードをより簡潔で読みやすくできます。
基本概念
イテレータの主な特徴:
- each系 - 要素を順番に処理(each, each_with_index, each_with_object)
- 変換系 - 新しいコレクションを生成(map, flat_map, collect)
- 選択系 - 条件に合う要素を抽出(select, reject, filter, find)
- 集約系 - 要素を集約して値を返す(reduce, inject, sum)
- 検査系 - 条件判定を行う(all?, any?, none?, one?)
基本的なイテレータ
# each - 基本的な繰り返し
[1, 2, 3].each do |n|
puts n
end
#=> 1, 2, 3
# map/collect - 変換して新しい配列を返す
result = [1, 2, 3].map { |n| n * 2 }
puts result.inspect #=> [2, 4, 6]
# select/filter - 条件に合う要素を抽出
result = [1, 2, 3, 4, 5].select { |n| n.even? }
puts result.inspect #=> [2, 4]
# reject - 条件に合わない要素を抽出
result = [1, 2, 3, 4, 5].reject { |n| n.even? }
puts result.inspect #=> [1, 3, 5]
# find/detect - 最初にマッチする要素を返す
result = [1, 2, 3, 4, 5].find { |n| n > 3 }
puts result #=> 4
# reduce/inject - 累積計算
sum = [1, 2, 3, 4, 5].reduce(0) { |acc, n| acc + n }
puts sum #=> 15
# より簡潔に
sum = [1, 2, 3, 4, 5].reduce(:+)
puts sum #=> 15
# all? - 全ての要素が条件を満たすか
result = [2, 4, 6].all? { |n| n.even? }
puts result #=> true
# any? - いずれかの要素が条件を満たすか
result = [1, 2, 3].any? { |n| n.even? }
puts result #=> true
# none? - 全ての要素が条件を満たさないか
result = [1, 3, 5].none? { |n| n.even? }
puts result #=> true
# one? - 1つだけ条件を満たすか
result = [1, 2, 3].one? { |n| n.even? }
puts result #=> true
インデックス付きイテレータ
# each_with_index - インデックス付きで繰り返し
["a", "b", "c"].each_with_index do |item, index|
puts "#{index}: #{item}"
end
#=> 0: a
#=> 1: b
#=> 2: c
# map.with_index - 変換とインデックス
result = ["a", "b", "c"].map.with_index { |item, i| "#{i}-#{item}" }
puts result.inspect #=> ["0-a", "1-b", "2-c"]
# with_index(offset) - オフセット指定
result = ["a", "b", "c"].map.with_index(1) { |item, i| "#{i}. #{item}" }
puts result.inspect #=> ["1. a", "2. b", "3. c"]
# each_with_object - アキュムレータ付き繰り返し
result = [1, 2, 3].each_with_object([]) do |n, acc|
acc << n * 2
end
puts result.inspect #=> [2, 4, 6]
# ハッシュを構築
result = ["a", "b", "c"].each_with_object({}) do |item, hash|
hash[item] = item.upcase
end
puts result.inspect #=> {"a"=>"A", "b"=>"B", "c"=>"C"}
# each_slice - 指定サイズごとに分割
[1, 2, 3, 4, 5, 6].each_slice(2) do |slice|
puts slice.inspect
end
#=> [1, 2]
#=> [3, 4]
#=> [5, 6]
# each_cons - 連続する要素を取得
[1, 2, 3, 4, 5].each_cons(3) do |cons|
puts cons.inspect
end
#=> [1, 2, 3]
#=> [2, 3, 4]
#=> [3, 4, 5]
数値イテレータ
# times - 指定回数繰り返し
5.times do |i|
puts i
end
#=> 0, 1, 2, 3, 4
# upto - 増加しながら繰り返し
1.upto(5) do |n|
puts n
end
#=> 1, 2, 3, 4, 5
# downto - 減少しながら繰り返し
5.downto(1) do |n|
puts n
end
#=> 5, 4, 3, 2, 1
# step - ステップ指定で繰り返し
0.step(10, 2) do |n|
puts n
end
#=> 0, 2, 4, 6, 8, 10
# 範囲でのstep
(0..10).step(3) do |n|
puts n
end
#=> 0, 3, 6, 9
ハッシュのイテレータ
hash = { a: 1, b: 2, c: 3 }
# each - キーと値を取得
hash.each do |key, value|
puts "#{key}: #{value}"
end
#=> a: 1
#=> b: 2
#=> c: 3
# each_key - キーのみ取得
hash.each_key do |key|
puts key
end
#=> a, b, c
# each_value - 値のみ取得
hash.each_value do |value|
puts value
end
#=> 1, 2, 3
# each_pair - each のエイリアス
hash.each_pair do |key, value|
puts "#{key}=#{value}"
end
# map - 新しい配列を生成
result = hash.map { |key, value| [key, value * 2] }
puts result.inspect #=> [[:a, 2], [:b, 4], [:c, 6]]
# transform_values - 値を変換
result = hash.transform_values { |v| v * 2 }
puts result.inspect #=> {:a=>2, :b=>4, :c=>6}
# transform_keys - キーを変換
result = hash.transform_keys { |k| k.to_s.upcase }
puts result.inspect #=> {"A"=>1, "B"=>2, "C"=>3}
# select - 条件に合うペアを抽出
result = hash.select { |key, value| value > 1 }
puts result.inspect #=> {:b=>2, :c=>3}
# reject - 条件に合わないペアを抽出
result = hash.reject { |key, value| value > 1 }
puts result.inspect #=> {:a=>1}
文字列のイテレータ
# each_char - 文字ごとに繰り返し
"abc".each_char do |char|
puts char
end
#=> a, b, c
# each_byte - バイトごとに繰り返し
"abc".each_byte do |byte|
puts byte
end
#=> 97, 98, 99
# each_codepoint - コードポイントごとに繰り返し
"abc".each_codepoint do |cp|
puts cp
end
#=> 97, 98, 99
# each_line - 行ごとに繰り返し
"line1\nline2\nline3".each_line do |line|
puts line.chomp
end
#=> line1, line2, line3
# chars - 文字の配列を返す
result = "hello".chars
puts result.inspect #=> ["h", "e", "l", "l", "o"]
# bytes - バイトの配列を返す
result = "hello".bytes
puts result.inspect #=> [104, 101, 108, 108, 111]
高度なイテレータ
# flat_map - map + flatten
result = [[1, 2], [3, 4]].flat_map { |arr| arr.map { |n| n * 2 } }
puts result.inspect #=> [2, 4, 6, 8]
# compact - nilを除去
result = [1, nil, 2, nil, 3].compact
puts result.inspect #=> [1, 2, 3]
# uniq - 重複を除去
result = [1, 2, 2, 3, 3, 3].uniq
puts result.inspect #=> [1, 2, 3]
# group_by - グループ化
result = [1, 2, 3, 4, 5, 6].group_by { |n| n % 3 }
puts result.inspect #=> {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}
# partition - 条件で分割
even, odd = [1, 2, 3, 4, 5].partition { |n| n.even? }
puts even.inspect #=> [2, 4]
puts odd.inspect #=> [1, 3, 5]
# take - 最初のn個を取得
result = [1, 2, 3, 4, 5].take(3)
puts result.inspect #=> [1, 2, 3]
# drop - 最初のn個をスキップ
result = [1, 2, 3, 4, 5].drop(2)
puts result.inspect #=> [3, 4, 5]
# take_while - 条件を満たす間取得
result = [1, 2, 3, 4, 1, 2].take_while { |n| n < 4 }
puts result.inspect #=> [1, 2, 3]
# drop_while - 条件を満たす間スキップ
result = [1, 2, 3, 4, 5].drop_while { |n| n < 3 }
puts result.inspect #=> [3, 4, 5]
# zip - 複数の配列を結合
result = [1, 2, 3].zip(["a", "b", "c"])
puts result.inspect #=> [[1, "a"], [2, "b"], [3, "c"]]
# cycle - 繰り返しサイクル
result = []
["a", "b"].cycle(2) { |item| result << item }
puts result.inspect #=> ["a", "b", "a", "b"]
# reverse_each - 逆順で繰り返し
[1, 2, 3].reverse_each do |n|
puts n
end
#=> 3, 2, 1
よくあるユースケース
ケース1: データ変換とフィルタリング
実務でよく使われるデータ処理のパターンです。
# ユーザーデータの処理
users = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 30, active: false },
{ name: "Charlie", age: 35, active: true },
{ name: "David", age: 28, active: true }
]
# アクティブユーザーの名前を取得
active_names = users.select { |u| u[:active] }.map { |u| u[:name] }
puts active_names.inspect #=> ["Alice", "Charlie", "David"]
# 年齢の平均を計算
average_age = users.map { |u| u[:age] }.reduce(:+) / users.size.to_f
puts average_age #=> 29.5
# 年齢でグループ化(20代、30代)
grouped = users.group_by { |u| u[:age] / 10 * 10 }
puts grouped.inspect
#=> {20=>[{:name=>"Alice", :age=>25, :active=>true}, {:name=>"David", :age=>28, :active=>true}], 30=>[{:name=>"Bob", :age=>30, :active=>false}, {:name=>"Charlie", :age=>35, :active=>true}]}
# 複数の条件でフィルタリング
result = users.select { |u| u[:active] && u[:age] >= 30 }
puts result.inspect #=> [{:name=>"Charlie", :age=>35, :active=>true}]
# チェーンして処理
result = users
.select { |u| u[:active] }
.map { |u| { name: u[:name], age_group: u[:age] / 10 * 10 } }
.group_by { |u| u[:age_group] }
puts result.inspect
#=> {20=>[{:name=>"Alice", :age_group=>20}, {:name=>"David", :age_group=>20}], 30=>[{:name=>"Charlie", :age_group=>30}]}
ケース2: 集計とレポート生成
# 売上データの集計
sales = [
{ product: "Apple", price: 100, quantity: 5 },
{ product: "Banana", price: 50, quantity: 10 },
{ product: "Apple", price: 100, quantity: 3 },
{ product: "Orange", price: 80, quantity: 7 }
]
# 商品ごとの合計売上
total_by_product = sales.each_with_object(Hash.new(0)) do |sale, totals|
totals[sale[:product]] += sale[:price] * sale[:quantity]
end
puts total_by_product.inspect
#=> {"Apple"=>800, "Banana"=>500, "Orange"=>560}
# 総売上
total_sales = sales.reduce(0) do |sum, sale|
sum + sale[:price] * sale[:quantity]
end
puts total_sales #=> 1860
# 最も売れた商品
best_seller = total_by_product.max_by { |product, total| total }
puts best_seller.inspect #=> ["Apple", 800]
# 平均単価
average_price = sales.map { |s| s[:price] }.sum / sales.size.to_f
puts average_price #=> 82.5
# 在庫レポート生成
inventory = sales.group_by { |s| s[:product] }
.transform_values { |items| items.sum { |i| i[:quantity] } }
puts inventory.inspect
#=> {"Apple"=>8, "Banana"=>10, "Orange"=>7}
# 詳細レポート
report = inventory.map do |product, qty|
price = sales.find { |s| s[:product] == product }[:price]
revenue = total_by_product[product]
{ product: product, quantity: qty, revenue: revenue, avg_price: price }
end
puts report.inspect
#=> [{:product=>"Apple", :quantity=>8, :revenue=>800, :avg_price=>100}, {:product=>"Banana", :quantity=>10, :revenue=>500, :avg_price=>50}, {:product=>"Orange", :quantity=>7, :revenue=>560, :avg_price=>80}]
ケース3: バリデーションとエラーチェック
# データのバリデーション
records = [
{ id: 1, name: "Alice", age: 25, email: "alice@example.com" },
{ id: 2, name: "", age: 30, email: "bob@example.com" },
{ id: 3, name: "Charlie", age: -5, email: "invalid" },
{ id: 4, name: "David", age: 28, email: "david@example.com" }
]
# 全てのレコードが有効か
all_valid = records.all? do |record|
record[:name] && !record[:name].empty? &&
record[:age] > 0 &&
record[:email].include?("@")
end
puts all_valid #=> false
# 無効なレコードを抽出
invalid = records.reject do |record|
record[:name] && !record[:name].empty? &&
record[:age] > 0 &&
record[:email].include?("@")
end
puts invalid.inspect
#=> [{:id=>2, :name=>"", :age=>30, :email=>"bob@example.com"}, {:id=>3, :name=>"Charlie", :age=>-5, :email=>"invalid"}]
# エラーメッセージを生成
errors = records.each_with_object({}) do |record, errs|
record_errors = []
record_errors << "名前が空です" if record[:name].empty?
record_errors << "年齢が不正です" if record[:age] <= 0
record_errors << "メールが不正です" unless record[:email].include?("@")
errs[record[:id]] = record_errors unless record_errors.empty?
end
puts errors.inspect
#=> {2=>["名前が空です"], 3=>["年齢が不正です", "メールが不正です"]}
# いずれかのフィールドにエラーがあるか
has_errors = records.any? do |record|
record[:name].empty? || record[:age] <= 0 || !record[:email].include?("@")
end
puts has_errors #=> true
ケース4: ネストしたデータの処理
# ネストした構造のデータ
departments = [
{
name: "Engineering",
teams: [
{ name: "Backend", members: ["Alice", "Bob"] },
{ name: "Frontend", members: ["Charlie", "David"] }
]
},
{
name: "Sales",
teams: [
{ name: "Enterprise", members: ["Eve", "Frank"] },
{ name: "SMB", members: ["Grace"] }
]
}
]
# 全メンバーのリストを取得
all_members = departments.flat_map do |dept|
dept[:teams].flat_map { |team| team[:members] }
end
puts all_members.inspect
#=> ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace"]
# 部門ごとのメンバー数
member_counts = departments.each_with_object({}) do |dept, counts|
total = dept[:teams].sum { |team| team[:members].size }
counts[dept[:name]] = total
end
puts member_counts.inspect
#=> {"Engineering"=>4, "Sales"=>3}
# チームとメンバーの対応表
team_members = departments.flat_map do |dept|
dept[:teams].map do |team|
{ department: dept[:name], team: team[:name], count: team[:members].size }
end
end
puts team_members.inspect
#=> [{:department=>"Engineering", :team=>"Backend", :count=>2}, {:department=>"Engineering", :team=>"Frontend", :count=>2}, {:department=>"Sales", :team=>"Enterprise", :count=>2}, {:department=>"Sales", :team=>"SMB", :count=>1}]
# 最大メンバー数のチーム
largest_team = team_members.max_by { |t| t[:count] }
puts largest_team.inspect
#=> {:department=>"Engineering", :team=>"Backend", :count=>2}
ケース5: 時系列データの処理
# 日次データの処理
daily_data = [
{ date: "2025-01-01", views: 100, clicks: 10 },
{ date: "2025-01-02", views: 150, clicks: 15 },
{ date: "2025-01-03", views: 120, clicks: 12 },
{ date: "2025-01-04", views: 180, clicks: 20 },
{ date: "2025-01-05", views: 200, clicks: 25 }
]
# 合計値を計算
total_views = daily_data.sum { |d| d[:views] }
total_clicks = daily_data.sum { |d| d[:clicks] }
puts "Total views: #{total_views}" #=> Total views: 750
puts "Total clicks: #{total_clicks}" #=> Total clicks: 82
# 平均CTRを計算
avg_ctr = (total_clicks.to_f / total_views * 100).round(2)
puts "Average CTR: #{avg_ctr}%" #=> Average CTR: 10.93%
# 日ごとのCTRを追加
with_ctr = daily_data.map do |d|
ctr = (d[:clicks].to_f / d[:views] * 100).round(2)
d.merge(ctr: ctr)
end
puts with_ctr.inspect
# 前日比を計算
with_change = daily_data.each_cons(2).map do |prev, curr|
change = ((curr[:views] - prev[:views]).to_f / prev[:views] * 100).round(2)
curr.merge(change: change)
end
puts with_change.inspect
# 移動平均を計算(3日間)
moving_avg = daily_data.each_cons(3).map do |window|
avg = window.sum { |d| d[:views] } / 3.0
{ date: window.last[:date], moving_avg: avg.round(2) }
end
puts moving_avg.inspect
#=> [{:date=>"2025-01-03", :moving_avg=>123.33}, {:date=>"2025-01-04", :moving_avg=>150.0}, {:date=>"2025-01-05", :moving_avg=>166.67}]
# 閾値を超えた日を検出
high_traffic_days = daily_data.select { |d| d[:views] > 150 }
puts "High traffic days: #{high_traffic_days.size}" #=> High traffic days: 2
注意点とベストプラクティス
注意点
- 破壊的メソッドとの違い
# BAD: 元の配列が変更されることを期待
arr = [1, 2, 3]
result = arr.map { |n| n * 2 } # 非破壊的
puts arr.inspect #=> [1, 2, 3](変更されない)
puts result.inspect #=> [2, 4, 6]
# GOOD: 破壊的メソッドを使う
arr = [1, 2, 3]
arr.map! { |n| n * 2 }
puts arr.inspect #=> [2, 4, 6]
# または変数を再代入
arr = [1, 2, 3]
arr = arr.map { |n| n * 2 }
puts arr.inspect #=> [2, 4, 6]
- パフォーマンスの考慮
# BAD: 複数回繰り返す
data = (1..1000).to_a
result1 = data.select { |n| n.even? }
result2 = result1.map { |n| n * 2 }
result3 = result2.sum
# GOOD: チェーンで一度に処理
result = (1..1000).select { |n| n.even? }.map { |n| n * 2 }.sum
# さらに良い: reduceで一度に処理
result = (1..1000).reduce(0) do |sum, n|
n.even? ? sum + (n * 2) : sum
end
- 副作用のある処理
# BAD: イテレータ内で外部の状態を変更
count = 0
[1, 2, 3].each do |n|
count += n # 副作用
end
# GOOD: reduceを使う
count = [1, 2, 3].reduce(0) { |sum, n| sum + n }
# またはsum
count = [1, 2, 3].sum
ベストプラクティス
- 適切なイテレータを選ぶ
# GOOD: 目的に応じたイテレータを使う
# 変換 → map
result = [1, 2, 3].map { |n| n * 2 }
# フィルタリング → select/reject
result = [1, 2, 3, 4].select { |n| n.even? }
# 検索 → find
result = [1, 2, 3, 4].find { |n| n > 2 }
# 集約 → reduce
result = [1, 2, 3].reduce(:+)
# 存在確認 → any?/all?
result = [1, 2, 3].any? { |n| n > 2 }
- メソッドチェーンの適切な使用
# GOOD: 読みやすいチェーン
result = users
.select { |u| u[:active] }
.map { |u| u[:name] }
.sort
# 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 }
# GOOD: 変数で分割
positive = data.select { |x| x > 0 }
doubled = positive.map { |x| x * 2 }
grouped = doubled.group_by { |x| x % 3 }
summed = grouped.transform_values { |v| v.sum }
filtered = summed.select { |k, v| v > 10 }
result = filtered.sort_by { |k, v| v }
- シンボルto_procを活用
# GOOD: シンプルな変換はシンボルで
result = ["hello", "world"].map(&:upcase)
# GOOD: 数値計算もシンボルで
sum = [1, 2, 3].reduce(&:+)
# BAD: 複雑な処理でシンボルを使わない
result = data.map(&:complex_calculation_with_many_steps) # 読みにくい
- 遅延評価を活用
# GOOD: 大量データには遅延評価
result = (1..1_000_000)
.lazy
.select { |n| n.even? }
.map { |n| n * 2 }
.take(10)
.to_a
# 無限シーケンスも扱える
result = (1..)
.lazy
.select { |n| n % 3 == 0 }
.take(5)
.to_a
Ruby 3.4での改善点
- YJITによるイテレータの最適化
- Prismパーサーによる高速な解析
-
itパラメータによる簡潔なブロック記法
# Ruby 3.4の新機能
# itパラメータで簡潔に書ける
result = [1, 2, 3].map { it * 2 }
puts result.inspect #=> [2, 4, 6]
# 従来の書き方
result = [1, 2, 3].map { |n| n * 2 }
まとめ
この記事では、Rubyのイテレータについて以下の内容を学びました:
- 基本的なイテレータ(each, map, select, reduce, find)
- インデックス付きイテレータ(each_with_index, each_with_object)
- 数値イテレータ(times, upto, downto, step)
- ハッシュのイテレータ(each_key, each_value, transform_values)
- 高度なイテレータ(flat_map, group_by, partition, zip)
- 実践的なユースケース(データ変換、集計、バリデーション、ネスト処理、時系列処理)
- 注意点(破壊的メソッド、パフォーマンス、副作用)
- ベストプラクティス(適切なイテレータ選択、メソッドチェーン、遅延評価)
適切なイテレータを使うことで、より簡潔で効率的なコードを書くことができます。
Discussion