【一人 cugs.ruby Advent Calendar 2020】[Feature #17016] Enumerable#scan_left【25日目】

一人 bugs.ruby Advent Calendar 2020 25日目の記事になります。
長かったアドベントカレンダーも今日で最後です。

[Feature #17016] Enumerable#scan_left

このチケットは Enumerable#scan_left を追加する提案です。
Enumerable#scan_leftEnumerable#inject と似たようなメソッドなのですが各ブロックの結果を配列で返します。

# inject は最後の結果だけ返す
[1, 2, 3].inject(0, &:+)
# => 6

# scan_left は各ブロックの戻り値を配列として返す
[1, 2, 3].scan_left(0, &:+)
# => [0, 1, 3, 6]

これは HaskellScala に存在する関数らしく Ruby でも欲しいそうです。
PR はちょっと前からあり、gem もあります。

ユースケースとしては累積和を求めるときに便利らしいです。
他にはコメントで以下のようなユースケーズも提示されています。

# 銀行の入出金履歴
gains = [+3000, -2000, +2000, -1000]

# 残高の履歴を計算
sums = [0]
(1..gains.length).each do |i|
  sums[i] = sums[i - 1] + gains[i - 1]
end
pp sums
# => [0, 3000, 1000, 3000, 2000]

# scan_left を使うとシュッとできる
sums = gains.scan_left(0, &:+)
pp sums
# => [0, 3000, 1000, 3000, 2000]

あとは以下のようなケースとか…。

module Enumerable
  # 疑似実装
  def scan_left(init = shift, &block)
    inject([init]) { |a, e| a << (block.call a.last, e) }
  end
end

# 4312.to_s.chars.sort.join.to_i の呼び出し過程を計算したりとか…
p [4312, :to_s, :chars, :sort, :join, :to_i].scan_left(&:send)
# => [4312, "4312", ["4", "3", "1", "2"], ["1", "2", "3", "4"], "1234", 1234]

ちなみに Enumerable#scan_left は以下のように #inject を使っても同じ値を取得する事はできます。

pp [1, 2, 3].inject([0]){ |a, e| a << a.last + e }
# => [0, 1, 3, 6]

ただし、この場合は普通には #lazy 化はできないので注意する必要があります。

# こういうような書き方はできない
(1..).lazy.inject([0]){|a, e| a << a.last + e} # => infinite loop
(1..).lazy.each_with_object([0]){|e, a| a << a.last + e} # => infinite loop
(1..).lazy.scan_left(0, &:+) # => Lazy enumerator

# がんばればできる
p (1..).lazy.enum_for(:inject, 0).map {|a, b| a + b }.take(10).force
# => [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

# もしくは
# p (1..).lazy.enum_for(:inject, 0).map {|a, b| a + b }.first(10)

#scan_left という名前はあんまりよろしくないと言うことで別の名前の提案がされいて今はそこで議論が止まっている感じです。
候補としては reflectproject interject tranject cumulative などなど…。
このあたりの名前決めは難しそうですねえ。