0xf

日記だよ

all? と any? 議論を眺めていて

一瞬、世の中で話題だったので、自分のメンタルモデルを棚卸しした。

前置き

この議論は「実業務において、配列の全ての要素に対して条件が満たされれば真という関数があるとき、空配列に対しては真である『べき』かどうか」という、なんともいえないところからスタート(そんな感じに読み取った)していて、実業務みたいなニュアンスとか「べき」の強さであるとか、一般論と半径2mの議論が入り混じり、素人とプロの意見が交錯していて、「この議論は私の土俵で行いましょう。土俵については説明不要だと思うので割愛します。私は玄人ですがあなたたちは素人です」というノリの人がエキサイトしており、気がついたら静まっていた。

まあこれ自体に関しては土俵が揃えばなんでもええわいと思っているし、土俵が揃った時点で自動的に決着する部類の話だと思うのでいいのだけど、

とはいえ自分はどうして「空配列に対しては真を返すのが自然」と感じるのだろうか、という点で疑問が残っていた。

ので考えてみる。といっても別にそんな長々としたもんではないけども。

本文

まず、前段として「集合のすべての要素に対して関数を適用した結果の集合」がある。[0,1,2,3,4] に対して「偶数かどうか」の関数を適用した結果の集合は [True,False,True,False,True] になる。

で、型Tの集合から一つの値を求める計算なので reduce する。真偽値同士の計算なのでブール演算だけでよい。

「すべてがAの条件を満たすか」はこう。(例ではA = 偶数かどうか)

>>> from functools import reduce

>>> reduce(lambda a,b: a and b, map(lambda _ : _ % 2 == 0 , []), True)
True

>>> reduce(lambda a,b: a and b, map(lambda _ : _ % 2 == 0 , [0,1,2,3]), True)
False

「一つでもAの条件を満たすか」は初期値を偽として or を次々ととっている。

>>> reduce(lambda a,b: a or b, map(lambda _ : _ % 2 == 0 , [0,1,2,3]), False)
True

>>> reduce(lambda a,b: a or b, map(lambda _ : _ % 2 == 0 , []), False)
False

というのが自分にとっては「直感的」ぽい。

では「空配列の時にエラーにしたいときはどうなるのか」という話なのだけど、

>>> reduce(lambda a,b: a or b, map(lambda _ : _ % 2 == 0 , [0]))
True

>>> reduce(lambda a,b: a or b, map(lambda _ : _ % 2 == 0 , []))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: reduce() of empty iterable with no initial value

reduceで初期値を指定しないときの動作はこうなる。つまり、「初期値がなくて空配列が相手だと例外」でそれはそう。自然に感じる。

ここで自分は「初期値を指定しないreduceは例外的」と考えているぽい。なぜなら空配列に対して動作が未定義だからだと思う。その方が自分にとっては気持ちが悪い。このpythonでのサンプルコードを読んで、初期値をわざわざ設定するのは空配列に対する例外的な設定ではないか、引数が省略可能というのは、省略する方が普通なのではないか、と感じる人がいても別に不思議ではない。

確かに、このあたりは感覚的なものがあるなー。

ということで

こういう感じ。

all = lambda f,t: reduce(lambda a,b: a and b, map(f, t), True)

any = lambda f,t: reduce(lambda a,b: a or b, map(f, t), False)

ただこれは概念的な説明のために map を使っているがどちらの関数も最後まで回さず False で決まるケースがある。繰り返し使われるならこういう実装にはしないかも。(そこは勝手に最適化されたいところではあるけども)