Enumerable#inject
の引数にシンボルを渡して畳み込み(fold)を短く書くことがよくある。演算子である +
や *
などが多いが、一定の条件を満たすメソッドなら何でもいい。普段見ないような使い方ができないか探ってみた。
ちなみに実用性ありそうなものは大抵もっといい方法がある。何でもinjectを使おうとするのは楽しいけど良くない。
ブロック省略の形
例えば #map
では以下のようにブロックを省略できる。
# 単純な処理ではこんな形になることがある
list.map { |item| item.mtd0 }
list.map { |item| object.mtd1(item) }
list.map { |item| hash_table[item] }
# 上のブロックは & を使って短縮できる
list.map(&:mtd0)
list.map(&object.method(:mtd1))
list.map(&hash_table) # Ruby2.3から
Symbol, Method, Hash と脈絡のない省略規則に見えるが、これらは各クラスの #to_proc
の定義に依存している。もちろん自分で定義してもいい。
#inject
でも同様のことができる。ブロックの引数が2つという違いに注意する。(※ init
は省略に関係ない)
# 単純な処理ではこんな形になることがある
list.inject(init) { |result,item| result.mtd1(item) }
list.inject(init) { |result,item| object.mtd2(result, item) }
# 上のブロックは & を使って短縮できる
list.inject(init, &:mtd1)
list.inject(init, &object.method(:mtd2))
上の省略は #each_with_index
などでもできるが、 #inject
は引数に直接シンボル(または文字列)を取れるようにも定義されている。
# injectは特別に以下の書き方もできる
list.inject(init, :mtd1) # この書き方が一般的
list.inject(init, "mtd1")
この mtd1
にどんなメソッドを使えるか見ていきたい。
ところで、総和を求めるときに :+
を渡すのは、演算子 +
がメソッドとして書けるため成り立っている。メソッドでない演算子 &&
などは不可。
list.inject(0, :+) <=> list.inject(0) { |result,item| result.+(item) }
メソッドの条件
ループを展開すればわかる。「メソッド呼び出し .mtd1(item)
に応答するオブジェクト」を返し続けられればいい。
init.mtd1(list[0])
.mtd1(list[1])
.mtd1(list[2])
...
.mtd1(list[-1])
しかし戻り値 result
のクラスが毎回変わると調査がきつい。そこで init
や result
のクラスは変わらないという前提条件をおく。すると、
-
result.class
はmtd1
というインスタンスメソッドを持つ -
mtd1
は引数を1つとる(可変長引数も可) -
mtd1
はブロック不要 -
mtd1
の戻り値はresult.class
である(継承やmix-inも考慮が必要)
という性質を満たす result.class#mtd1
を探せばいいことになる。これくらいならマニュアルを漁って調べられそう。
例
全部列挙すると無意味なものまで入ってしまうので、ある程度面白そうなものを挙げる。一部はinjectを使わない方法も言及する。
Numeric, Integer
四則演算やビット演算など、ほとんどの二項演算はinjectで使える。演算子が無いものだと最大公約数あたりが便利。
# 最大公約数と最小公倍数
[105, 140, 168].inject(0, :gcd) #=> 7
[ 12, 15, 20].inject(1, :lcm) #=> 60
+
の場合は sum
を使ったほうがいい(Ruby2.4以上)。
([0.1] * 10000).inject(0, :+) #=> 1000.0000000001588
([0.1] * 10000).sum(0) #=> 1000.0
String
+
, <<
など結合するものはわかりやすい。でも join
や concat
で済む。
["ab", "cd", "ef"].inject("", :<<) #=> "abcdef"
"".concat("ab", "cd", "ef") #=> "abcdef"
concat
とは逆に、可変長引数を利用すると意味が変わるメソッドがあった。
# 文字の除去
["2-6", "^4-8"].inject("123456789", :delete) #=> "78"
"123456789".delete("2-6", "^4-8") #=> "1456789"
# squeezeも異なる結果になる
Enumerable, Array, Hash
String 同様 +
, <<
など結合するものはわかりやすい。 flatten(1)
で済んだり、 concat
や push
のように可変長引数なものがあるのも同じ。 Hash#merge
は代替手段が無さそうRuby2.6から複数のマージに対応した。
# ハッシュの結合
[{a: 0, b: 1}, {a: 2, c: 3}, {b: 4, c: 5}].inject({}, :merge)
#=> {:a=>2, :b=>4, :c=>5}
# Ruby2.6以降
{}.merge({a: 0, b: 1}, {a: 2, c: 3}, {b: 4, c: 5})
#=> {:a=>2, :b=>4, :c=>5}
Array, Enumerator, Object という風に色々なオブジェクトを返すメソッドがあるので、injectも色々なパターンができる。
# and検索
[/in/, /[^a-z]/, /^.{,8}$/].inject(777.methods, :grep)
#=> [:to_int, :integer?, :finite?, :kind_of?, :tainted?]
# 多次元配列化
md_ary = [3, 2].inject(0...12, :each_slice).to_a
#=> [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]]]
# 多次元配列の要素を参照
[0, 1, 2].inject(md_ary, :[]) #=> 5
[1, 2, 3].inject(md_ary, :[]) #=> NoMethodError (undefined method `[]' for nil:NilClass)
[1, 2, 3].inject(md_ary, :fetch) #=> IndexError (index 2 outside of array bounds: -2...2)
[]
と fetch
は Array と Hash の両方にあるので、JSONのように互いに入れ子になっていても大丈夫。Ruby2.3以降は dig
で一気に参照できる。
md_ary.dig(0, 1, 2) #=> 5
md_ary.dig(1, 2, 3) #=> nil
TrueClass と FalseClass
論理演算子 &
, |
, ^
が共通して存在し、戻り値もどちらかになるのでinjectできる。戻り値のクラスが異なってもいい単純な例。
list = [true, false, nil, 0, ""]
list.inject(true , :&) #=> false
list.inject(false, :|) #=> true
list.inject(false, :^) #=> true
AND, ORは直接的に求められるメソッドがある。XORは「真の個数の偶奇」を考える。
list.all? #=> false
list.any? #=> true
list.count(&:itself).odd? #=> true
BasicObject
全てのクラスの親である BasicObject のインスタンスメソッドなら、ほぼ全てのオブジェクトに対して使える。
# メソッドを連続して作用させる
%i(class singleton_class superclass).inject(777, :__send__) #=> #<Class:Numeric>
# 任意のコードの結果を次のコードの `self` にする
(<<EOS.lines).inject(777, :instance_eval)
p self.class
p singleton_class
p superclass
EOS
その他
injectは引数をひとつだけとることも可能なので、injectのinjectもできる。とはいえ真っ当な例を見つけられていない。
%i(divmod coerce divmod).inject([1000, 7], :inject) #=> [0, 6]
せっかくブロック省略で紹介したので mtd2
の例もひとつ。
# n次元ベクトルの長さ
[8, 9, 12].inject(0, &Math.method(:hypot)) #=> 17.0
[8, 9, 12].sum(&:abs2) ** 0.5 #=> 17.0
Vector[8, 9, 12].norm #=> 17.0