💎

【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

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

注意点

  1. 破壊的メソッドとの違い
# 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]
  1. パフォーマンスの考慮
# 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
  1. 副作用のある処理
# 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

ベストプラクティス

  1. 適切なイテレータを選ぶ
# 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 }
  1. メソッドチェーンの適切な使用
# 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 }
  1. シンボルto_procを活用
# GOOD: シンプルな変換はシンボルで
result = ["hello", "world"].map(&:upcase)

# GOOD: 数値計算もシンボルで
sum = [1, 2, 3].reduce(&:+)

# BAD: 複雑な処理でシンボルを使わない
result = data.map(&:complex_calculation_with_many_steps)  # 読みにくい
  1. 遅延評価を活用
# 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