LoginSignup
5
5

More than 3 years have passed since last update.

injectのシンボルに指定できるメソッド色々

Last updated at Posted at 2018-06-21

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 のクラスが毎回変わると調査がきつい。そこで initresult のクラスは変わらないという前提条件をおく。すると、

  • result.classmtd1 というインスタンスメソッドを持つ
  • 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

+, << など結合するものはわかりやすい。でも joinconcat で済む。

["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) で済んだり、 concatpush のように可変長引数なものがあるのも同じ。 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
5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5