見出し画像

なぜall([])がtrueなのか思いつく限り書いてみる

経緯はよく知らないのだが、先日、Twitterで、all([])がtrueになる、という話が話題になった。

この記事において、allとは、boolのリストを引数に取り、リストのすべての要素がtrueの場合にtrueを返し、また、そうでない場合にfalseを返す関数を指す。こういった関数は多くのプログラミング言語に用意されており、この記事での考え方は、プログラミング言語によらず応用できるが、記事中に出てくるコードはPythonを念頭に置いている。

def all(xs: list[bool]) -> bool:
    ...

私はall([])がtrueになるのを、論理的にそうなるというよりは、そのように定義されている、という程度の話だと思っている。しかし、そう定義されているのは、それが妥当であるからだ。

なぜ妥当なのか、思いつく限り並べてみよう。

1: allがfalseにならない場合を考えてみる

「allがtrueになる」というのは、「allがfalseにならない」ということだ。では、allがfalseにならないのはどんな場合だろう。

リストxsがあったとして、all(xs)はxsの全ての要素がtrueである場合にtrueとなり、そうでない場合にはfalseになる。すなわち、xsの要素のうち、ひとつでもfalseとなる要素がある場合falseとなり、そうでない場合trueとなる。

このことに納得が行くならば、all([])がtrueになることも納得できるはずである。なぜならば、[]には、そもそも要素がひとつもないのだから、falseとなる要素はひとつもない。よって、all([])はtrueとなる。

2: trueになるものの数を数えてみる

xsの要素のうち、trueになるものの数をnとする。
このとき、all(xs)がtrueとなることは、n == len(xs)となることに等しい

これは、xsの長さが有限とは限らない場合には誤っているが、有限である場合には、納得できるのではないか。そうだとすると、all([])はtrueになる。

[]のうち、trueになるものの数は0で、また、len([])は0なので、n == len(xs)が成り立つ。よって、all([])はtrueである。

3: 数理論理学用語としての「すべての」と対応させてみる

allの定義が、数理論理学での「すべての」と同じ意味を持っていると考えることは自然である。(必ずしもそのようになっている必要はないと言われてしまうとそうかもしれない)
そう思うのであれば、やはりall([])はtrueとなる。以下のように捉えよう。

3-1: all(xs)が「すべてのxについて、xがxsに含まれているならば、xはtrueである」と対応していると捉える

数理論理学において「falseならばP」は、Pが何であるかによらず常に真だと定められている。また、xsが空の場合、「xがxsに含まれている」は常にfalseである。よって、すべてのxについて、「xがxsに含まれているならば、xはtrueである」はtrueである。ゆえに、all(xs)はtrueである。

3-2: all(xs)が「すべてのx∈xsについて、xはtrueである」と対応していると捉える

これは、記法が3-1と違うだけで同じことを言っている。こちらの記法を使って、「すべてのx∈∅について、P」が真であることを言う人も多い。

4: 日本語で説明してみる

以下は、そうだと思うのではないだろうか。

  • 会社で経費申請する領収書をすべて提出しないといけない。申請する領収書が1枚もなければ、領収書は1枚も提出しなくてもよい

  • 引っ越しで、部屋の荷物をすべて引き上げたら、管理会社の人に確認をしてもらう。元々部屋に何も荷物がない場合、何もしなくても管理会社の人はOKをくれるはずだ

  • 子供に、すべての宿題が終わったら遊びに行っても良い、と言う。宿題がなかった日は、何もしなくても遊びに行って良い

それならば、all([])はtrueだと思うのではないだろうか。

5: 反復的な実装をしてみる

all()を実装すると、通常は以下のようになる。リストが空だった場合の特別な処理を付け加えない限りは、同様になるはずだ。

def all(xs: list[bool]) -> bool:
    for x in xs:
        if not x:
            return False
    return True

この場合、all([])はtrueになる。

6: 再帰的な実装をしてみる

例えば、1〜nまでの整数の積を求める階乗計算を、次のように実装することが多い。

def fact(n: int) -> int:
    if n == 0:
        return 1
    return n * fact(n - 1)

このとき、n == 0での結果を1に決め打ちしている。「1から0までの整数の積」が1になるというのは、あまり直感的ではないかもしれないが、そのように考えると見通しがいいし、それが階乗の定義にもなっている。

同様にall()を再帰的に実装することを考える。

def all(xs: list[bool]) -> bool:
    if not xs:
        return True
    return xs[0] and all(xs[1:])

階乗と同様に、xsが空のとき(Python風に書くとnot xsのとき)にtrueであると考えると見通しがいい。

7: 単位元を考える

再帰的に階乗を実装する場合にn == 0で1にするとなぜ見通しがいいのか、all()を実装する場合にxsが空のときにtrueだとなぜ見通しがいいのか、その大きな理由は、それらが、それらの演算の「単位元」となっているからだ。

要素eが演算★の単位元であるとは、すべての要素xに対して、
e★x == x
x★e == x
が成り立つことをいう。
数値の足し算 + の場合、単位元は0であり、掛け算 * の場合、単位元は1である。

二項演算を任意の個数だけ繰り返す場合において、0個のものを演算した結果は、単位元を返すのが自然である。
例えば、sum([])は、0となることを期待するだろう。

allは、二項演算 and を任意の個数だけ繰り返す関数である。そして、andの単位元はtrueである(すべてのxに対して、true and x == xとx and true == xが成り立つ)。

なので、all([])はtrueとなるのが自然である。

8: リストの連結を考える

2つのリストxsとysがあったとき、
sum(xs) + sum(ys) == sum(xs + ys)
となっていることは当然だろう。

同じように、
all(xs) and all(ys) == all(xs + ys)
となっていてほしい。

そのようになるには、all([]) == trueとなるしかない。

なぜならば、これが成り立つとき、
all([]) and all([true]) == all([true])
となっているはずで、すなわち、
all([]) and true == true
であるからだ。

結論

様々な観点から、all([])はtrueとすることは妥当である。


この記事が気に入ったらサポートをしてみませんか?