Tech Racho エンジニアの「?」を「!」に。
  • 開発

Ruby: Enumerableを`reduce`で徹底理解する#2 -- No-OpとBoolean(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

訳注: 原文ではRubyのメソッドを「function」と表記しています。本著者の記事では原文に沿って「関数」と表記します。

Ruby: Enumerableをreduceで徹底理解する#2 -- No-OpとBoolean(翻訳)

前回

前回の投稿では、Enumerableのいくつかの基本的な関数をreduceの観点から学びました。

参考までに、前回学んだものをリストアップします。

map

def map(list, &fn)
  list.reduce([]) { |a, i| a.push(fn[i]) }
end

map([1,2,3]) { |i| i * 2 }
# => [2, 4, 6]

select

def select(list, &fn)
  list.reduce([]) { |a, i| fn[i] ? a.push(i) : a }
end

select([1,2,3]) { |i| i > 1 }
# => [2, 3]

find

def find(list, &fn)
  list.reduce(nil) { |_, i| break i if fn[i] }
end

find([1,2,3]) { |i| i == 2 }
# => 2

find([1,2,3]) { |i| i == 4 }
# => nil

上では、reduceの結果を配列にしたり、reduceする値を完全に無視したりしました。

ここで特に注意を払うべき事実は、アキュムレータ(accumulator: 累算器)を無視していることです。というのも、アキュムレータを無視することでreduceの振る舞いについて実に興味深い観察結果を得られるからです。

今回の学習内容

今回はEnumerableのno-op関数やboolean関数を扱います。

「no-op」関数

findは何かを行うときにだけbreakで脱出するので何も蓄積しませんが、reduceはno-op(=何もしない)関数にも利用できます。たとえばeachがそうです。

def each(list, &fn)
  list.reduce(nil) { |_, i| fn[i] }
  list
end

eachは技術的に言えばEnumerableの関数ではありませんが、Enumerableのその他すべての関数はeachを下敷きにしています。すなわち、この実装はその制約を満たしていません。

boolean関数

reduceの楽しみのひとつは、boolean(論理値)はもちろんビット操作までreduceで行えることです。

any?all?none?one?include?member?といった関数がboolean関数です。

いくつか見てみましょう。

any?

def any?(list, &fn)
  list.reduce(false) { |_, i|
    break true if fn[i]
    false
  }
end

any?([1,2,3]) { |i| i > 5 }
# => false

any?([1,2,3]) { |i| i > 1 }
# => true

ここで注意が必要なのは、breakでは三項演算子がうまく動かない点です。ここでif x thenと書くのはどうもよろしくない感じがします。

findと同様、any?も何かを検出するとbreakします。

!!を使えばシンプルにワンライナーで書けますが、私は本シリーズではあえて冗長性を選ぶことにします。

def any?(list, &fn)\
  !!list.reduce(nil) { |_, i| break true if fn[i] }
end

all?

def all?(list, &fn)
  list.reduce(true) { |a, i| a && fn[i] }
end

all?([1,2,3]) { |i| i > 0 }
# => true

all?([1,2,3]) { |i| i > 2 }
# => false

all?&&演算子とboolean値を用いて畳み込みを行います。ここではすべての要素が述語の関数と一致するかどうかをチェックしています。しかしながら、この最初の実装は何だかおかしい気がします。どこが問題かおわかりでしょうか?

要素の中にfalseになるものがあれば、おそらく上述の関数と同様にそこでbreakすべきです。

def all?(list, &fn)
  list.reduce(true) { |a, i|
    break false unless fn[i]
    true
  }
end

all?([1,2,3]) { |i| i > 0 }
# => true

all?([1,2,3]) { |i| i > 2 }
# => false

none?

none?の動作は基本的にany?の逆です。none?が欲しければany?を用いて定義できます。

def none?(list, &fn)
  !any?(list, &fn)
end

none?([1,2,3]) { |i| i > 2 }
# => false

none?([1,2,3]) { |i| i > 5 }
# => true

理想的には既にあるものを再利用すべきですが、本シリーズにおいてはあくまでreduceでならどう実装できるかを見ていくことにします。

def none?(list, &fn)
  list.reduce(false) { |_, i|
    break false if fn[i]
    true
  }
end

none?([1,2,3]) { |i| i > 2 }
# => false

none?([1,2,3]) { |i| i > 5 }
# => true

ご覧のとおり、基本的には最後に条件を切り替えるだけです。

one?

def one?(list, &fn)
  list.reduce(false) { |a, i|
    if fn[i]
      break false if a
      true
    else
      a
    end
  }
end

one?([1,2,3]) { |i| i == 2 }
# => true

one?([1,2,3]) { |i| i > 1 }
# => false

one?([1,2,3]) { |i| i > 5 }
# => false

おまけです。one?は1件だけのマッチかどうかを調べる関数です。今度はこの関数の述語がtrueであったかどうかをチェックするだけではなく、それまでtrueでなかったかどうかも調べなければなりません。

こういう動作なので、件数が複数のときだけはいつもbreakすればよいというわけにはいきません。reduceで整数を1つ得て件数分実行するという手もなくはありませんが、既にbooleanステータスがあるのですからそれを使いましょう。

include?member?

def include?(list, item)
  list.reduce(false) { |a, i|
    break true if i == item
    false
  }
end

include?([1,2,3], 1)
# => true

include?([1,2,3], 5)
# => false

include?member?の動作は同じなのでまとめて扱います。

findと同じような要領で、該当する項目が1件あるかどうかを検索し、1件もなければfalseを返さなければなりません。

まとめ

次回はminmax、ソートなどIntegerのステートを扱う関数を取り上げます。

関連記事

Ruby: Enumerableを`reduce`で徹底理解する#1 基本編(翻訳)

Rubyで関数型プログラミング#1: ステート(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。