【一人 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_left
は Enumerable#inject
と似たようなメソッドなのですが各ブロックの結果を配列で返します。
# inject は最後の結果だけ返す [1, 2, 3].inject(0, &:+) # => 6 # scan_left は各ブロックの戻り値を配列として返す [1, 2, 3].scan_left(0, &:+) # => [0, 1, 3, 6]
これは Haskell や Scala に存在する関数らしく 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
という名前はあんまりよろしくないと言うことで別の名前の提案がされいて今はそこで議論が止まっている感じです。
候補としては reflect
や project
interject
tranject
cumulative
などなど…。
このあたりの名前決めは難しそうですねえ。