概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Reducing Enumerable — No-Op and Boolean – Brandon Weaver – Medium
- 原文公開日: 2017/10/16
- 著者: Brandon Weaver
訳注: 原文では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
を返さなければなりません。
まとめ
次回はmin
、max
、ソートなどInteger
のステートを扱う関数を取り上げます。