はじめに:Rubyで添え字付きでループを回し、かつ0以外の数値で開始する方法
配列や範囲(range)をループさせながら、添え字(インデックス)も同時に取得したい場合はeach_with_index
を使います。
以下は「プロを目指す人のためのRuby入門」の4.8.1項で使用したサンプルコードです。
fruits = ['apple', 'orange', 'melon']
# ブロック引数のiには0、1、2・・・と要素の添え字が入る
fruits.each_with_index { |fruit, i| puts "#{i}: #{fruit}" }
#=> 0: apple
# 1: orange
# 2: melon
each_with_index
では0から添え字が始まりますが、要件によっては0以外の数値から始めたい場合もあります。その場合はeach_with_index
の代わりにeach.with_index
(each
+ with_index
)を使います。
以下は「プロを目指す人のためのRuby入門」の4.8.3項で使用したサンプルコードです。
fruits = ['apple', 'orange', 'melon']
# eachで繰り返しつつ、1から始まる添え字を取得する
fruits.each.with_index(1) { |fruit, i| puts "#{i}: #{fruit}" }
#=> 1: apple
# 2: orange
# 3: melon
人によっては「いや、そうじゃなくてeach_with_index(1)
って書きたいんだけど!」と思うかもしれません。ですが、実際にそんなコードを書くとエラーになります。
fruits = ['apple', 'orange', 'melon']
fruits.each_with_index(1) { |fruit, i| puts "#{i}: #{fruit}" }
#=> ArgumentError (wrong number of arguments (given 1, expected 0))
そのため、「プロを目指す人のためのRuby入門」では「each_with_index
メソッドには引数を渡せない」と書いていました(4.8.3項参照)。
が!
each_with_index
に引数を渡してもエラーにならないケースがあります。
たとえば次のようなコードを書いた場合は、each_with_index
に引数を渡してもエラーになりません。
require "stringio"
sio = StringIO.new('apple,orange,melon')
# each_with_indexの引数を渡してもエラーにならない!
sio.each_with_index(',') {|fruit, i| puts "#{i}: #{fruit}" }
#=> 0: apple,
# 1: orange,
# 2: melon
なぜeach_with_index
に引数を渡せる場合と渡せない場合があるのでしょうか?
each_with_index
に引数を渡すと、そのままeach
メソッドに渡される
実はeach_with_index
に引数を渡すと、内部的にレシーバ(each_with_index
を呼びだしたオブジェクト)のeach
メソッドにその引数が渡されます。
fruits = ['apple', 'orange', 'melon']
# 内部的にfruits.each(1)が呼ばれる(エラー)
fruits.each_with_index(1) { |fruit, i| puts "#{i}: #{fruit}" }
require "stringio"
sio = StringIO.new('apple,orange,melon')
# 内部的にsio.each(',')が呼ばれる(OK)
sio.each_with_index(',') {|fruit, i| puts "#{i}: #{fruit}" }
そして、クラスによってはeach
メソッドに引数を渡せるものがあります。
たとえば、先ほど出てきたStringIOクラスはeach
メソッドに行の区切り文字を引数として渡せるようになっています(省略した場合は改行区切りになります。ドキュメントはこちらです)。
そのため、レシーバのeach
メソッドに引数が渡せるかどうかによって、each_with_index
メソッドに引数が渡せるか渡せないかが決まります。
結局each_with_index
単体では添え字の開始値を指定できない
each_with_index
メソッドに引数を渡せるケースと渡せないケースがあることはわかりましたが、そもそもやりたかったのはfruits.each_with_index(1)
のように、添え字の開始値を指定することでした。
ですが、「each_with_index
メソッドに引数を渡した場合はeach
メソッドに引数が渡される」という仕様になっているため、結局each_with_index
メソッドで添え字の開始値を指定することはできません。
なので、必ずeach.with_index(1)
のように、each
+ with_index
の形で書く必要があります。
おまけ:モンキーパッチでeach_with_index
の挙動を変えてしまう💀
「each
メソッドに引数を渡すようなケースなんて滅多にないから、なんとかしてeach_with_index
に直接添え字の開始値を指定したい!」という場合は、次のようなモンキーパッチを書いてeach_with_index
の挙動を変えてしまうこともできます。
# 注意:こんなモンキーパッチはあまりお勧めしない
module Enumerable
# オリジナルの実装を上書きして引数の仕様を変えてしまう
def each_with_index(offset = 0, &b)
each.with_index(offset, &b)
end
end
fruits = ['apple', 'orange', 'melon']
# 添え字の開始値が直接指定できるようになった
fruits.each_with_index(1) { |fruit, i| puts "#{i}: #{fruit}" }
#=> 1: apple
# 2: orange
# 3: melon
ただし、自分で書いたコードはこれでよくても、ライブラリ等の自分の見知らぬ場所で引数付きのeach_with_index
が使われていると、整合性が合わなくなってエラーが起きる可能性があります。なので、これはあまり好ましい対応ではありません。
単純なモンキーパッチではなく、refinementsを使うとそのファイル内(もしくはクラス内)だけに影響範囲を留めることができます。
# 以下のコードはirbではなくファイルに保存して実行する
module MyEachWithIndex
refine Enumerable do
def each_with_index(offset = 0, &b)
each.with_index(offset, &b)
end
end
end
using MyEachWithIndex
fruits = ['apple', 'orange', 'melon']
# このファイル内でのみ、添え字の開始値が直接指定できる
fruits.each_with_index(1) { |fruit, i| puts "#{i}: #{fruit}" }
が、個人的にはそこまで頑張らなくても、素直にeach.with_index(1)
と書く方が良いと思います。
なお、モンキーパッチやrefinementsについては「プロを目指す人のためのRuby入門」の中でも説明しています(7.10.6項および8.9.5項参照)。
まとめ
というわけで、この記事ではeach_with_index
メソッドで引数を指定できるケースがあることを説明しました。
ただし、日常的なプログラミングでeach_with_index
メソッドに引数を渡す(つまり、each
メソッドに引数を渡す)ケースはあまりないと思います。
ですので、「each_with_index
メソッドには引数を渡せない」と覚えていてもほとんど問題ないと思いますが、例外的なケースがあることも知っておくと、どこかで役に立つかもしれません。
参考文献
実は僕自身も@znzさんのこちらのコメントを読むまでは、each_with_index
に引数が渡せることを知りませんでした😅
今回の記事は、@znzさんが上記のコメントの中で紹介していたこちらの議論を、僕なりにかみ砕いてまとめた内容になっています。
each_with_index
に引数が渡せることは、英語版のAPIドキュメントで説明されています(2018年8月時点では、日本語版には載っていません)。
each_with_index(*args) { |obj, i| block } → enum
each_with_index(*args) → an_enumeratorCalls block with two arguments, the item and its index, for each item in enum. Given arguments are passed through to each().