Chienomi

論理XORを書く

プログラミング::essential

XORとは

XORは「AまたはBであり、AおよびBでない」である。

否定のNOR, NAND, NXORを含めると次のような関係になる。

演算子名 一般的な論理表現 真/真 真/偽 偽/偽
AND A && B true false false
OR A || B true true false
XOR A ^^ B false true false
NAND !(A && B) false true true
NOR !(A || B) false false true
NXOR !(A ^^ B) ture false true

だが、ビット排他的論理和 ^ は一般的にあるのに対し、排他的論理和 ^^ 演算子はかなり稀である。

^^がない場合の方法は色々あるのだが、そのやり方は言語によりいくらか異なる。 つまり、定石や知識の範囲よりも、工夫の範囲であると言えるだろう。 そこで、ビギナー向けに排他的論理和を書く方法について述べよう。

ピンとこない人のために言うと、これは

if ((a && !b ) || (!a && b)) {
  //...
}

のようなダサいコードを書かないための知識である。

注意点

A ^^ BA && BA || Bと違い、短絡評価されない

A, B共に常に1回評価される。これが守られない書き方は代えとして適用できないので注意が必要だ。

一般的な書き方

よくあるのは

!A != !B

または

!!(!A ^ !B)

である。

まず、論理評価をするにあたり「booleanしか評価できない」という言語は非常に稀であり、大抵は論理値以外も何らかの論理評価が可能である。

だが、XORを実現するためには論理値でなければ都合が悪い。値の一貫性がないからだ。 そこで活用できるのが論理否定!である。

論理否定した場合、例え「真の値はなんでも良い」という場合でも、「偽の反対」は代表的な真にならざるを得ず、結果論理反転は(真偽を反転させた)booleanを得る。

前者は真偽が反転した状態になっているが、true ^^ falsefalse ^^ trueは同じ結果になるため、真偽が反転した状態で比較しても構わない。 ここで値はその言語のbooleanであるから、「等しくなければXOR真」という単純な考え方で良い。

後者は、1回論理反転した状態でビット排他的論理和を得て、それをさらに2回論理反転させる。 2回反転すると元の値からbooleanを得られる(もちろん、演算子や関数によって得られる言語もあるが、より汎用性があり、短い)。

カッコ内はture ^ falseが可能である必要がある。 真偽値に0, 1を使うような言語(例えばCやPerl)であれば話は簡単だが、純粋な論理値を持つ言語だと使えない可能性もある。

そして、true ^ falseで何を得られるか。一般的にはCなどの流儀に合わせてtrue1false0としたビット排他的論理和が得られるのが一般的だが、true ^ falseでbooleanが得られる言語もある。

ビット排他的論理和扱いで0または1が得られるとして、次の問題は0が偽かどうかである。 0が偽なのであれば、ture及びfalseが別に存在するとしても、論理コンテキスト上では0は偽なのだから、そのまま論理コンテキストに入れてしまえばいい。 しかし、0が真の言語においてはこれを論理値にする必要があり、!!でboolean化してもtrueになってしまう。!!()はこうした問題を解消するというよりは、booleanにするための実質無用な部分である。

実際に書ける内容

Perl

!$a ^ !$b
!$a != !$b

お手本通り。

Perlは論理値として0または1が得られるため、教科書通りの扱いになる。

Ruby

!a ^ !b

RubyにはTrueClass及びFalseClass^メソッドが実装されており、booleanになってさえいれば^メソッドによりXORが取れる。

JavaScript (Node14)

!a ^ !b
!a != !b
Boolean(a) ^ Boolean(b)
Boolean(a) != Boolean(b)

教科書通りでOK。

JavaScriptは0が偽になるため、!0trueになる。 そして、基本的にbooleanは数値で考えるタイプの言語であり、ture ^ false1になる。

PHP7

!$a ^ !$b
!$a != !$b

PHPも普通に見えるが、実はあんまり普通ではない。

PHPはPerlのような暗黙の変換が行われるのだが、なんとtrue表現は1なのに対し、false表現は""である!!

すごく気持ち悪いが、 !""1であり、 "" を数値コンテキストで評価すると0になるため、結果としては普通のやり方が通用する。

Python3

bool(a) ^ bool(b)

Pythonの場合、booleanによる排他的論理和を得ることが可能。

!ではなくnotと書く必要があるため、反転を用いるのは動機に乏しい。

Lua

(not a) ~= (not b)

Luaはbooleanがあり、boolean間で^が使用できない上に、0は真な言語であるため、この書き方しかない。 論理値を得る方法は他にもあるかもしれない。

Nim

a xor b

NimにはちゃんとXORがある。すごいね、Nim。

Nimは論理演算子で関係をまたぐことはできないので注意。 また、そもそも論理コンテキストではbool型であることが要求されるのも注意。 int型ならどうにかできるが、stringとかだとダメ。

Raku

$a ^^ $b

AURのRakudoが更新されてインストールできるようになった記念にRakuでもやってみた。

Rakuには^^が入っている。やったね。

bash

(( !a != !b ))

Bashの論理コンテキストは実行結果のステータスコードである。 この論理評価は基本は「その場で評価する」ものであり、後から評価に使用できない。 XORは短絡評価せず、「とりあえず両方評価してから比較する」ものになるため、「2つ前」になる左辺のコマンド実行結果を得るのは難しいところだ。

直前のコマンドのステータスコードは$?で得られるため、それを取っておけば、arithmetic evalutionによって扱うことができるようになる。

Bashでの真偽の考え方などは一般的なプログラミング言語とは全く異なるものだが、算術評価の中ではCなどに似た、割とポピュラーな考え方になり、例えば!1270になるし、!01になる。

ここまで踏まえれば割と普通な書き方が可能だ。