Common Lispには、さまざまな等価性の述語があります。
他のプログラミング言語では、1つか2つしか持たないことが多いのとは異なります(たとえば==と===など)。
Common Lispのこれらの述語には、型に固有のものもあれば、汎用的なものもあります。
ここでは、後者の汎用的な述語を取り上げます。
汎用的な等価性の述語は4つあり、「等しい」と見なす範囲の厳しさがそれぞれ異なります。
厳しいものから緩いものへ順に並べると、eq、eql、equal、equalpです。
簡単な定義は次のとおりです(細かい点はいくつか省きます)。
eq: 2つのオブジェクトが同一であることを「等しい」と定義します。
例:(eq 'a 'a) ; => T
(eq (list 1 2) (list 1 2)) ; => NIL
eql: 同じ型と同じ値を持つ2つの数値、同じ2つの文字、それ以外についてはeqであることを「等しい」と定義します。
例:(eql 1 1) ; => T
(eql #\a #\a) ; => T
(eql 1 1.0) ; => NIL
(eql #\a #\A) ; => NIL
equal: 「等しい」を次のように定義します。2つのリストは、それぞれの要素もequalであればequalです。2つの文字列は、それぞれの要素がeqlであればequalです。2つの配列は、eqであればequalです。それ以外はすべて、eqlであればequalです。
例:(equal (list 1 2) (list 1 2)) ; => T
(equal #(1 2) #(1 2)) ; => NIL
(equal "foo" "foo") ; => T
equalp: 「等しい」を次のように定義します。文字列と文字は大文字と小文字を区別せずに比較されます。数値は何らかの型変換を伴って比較されます。リストと配列は、すべての要素がequalpであればequalp、構造体は同じ型ですべてのスロットがequalpであればequalp、ハッシュテーブルはキーと値がequalpであればequalpです。
例:(equalp "foo" "FoO") ; => T
(equalp 3 3.0) ; => T
(equalp (make-a-structure :slot1 1 :slot2 2)
(make-a-structure :slot1 1 :slot2 2)) ; => T
Lisp星人のレスリーは、迷路ロボットをプログラミングしています(若いLisp星人にはよくあることです)。少し助けが必要なようです。迷路はいくつかの部屋からできています。各部屋には、選べるドアがいくつかあります。それぞれのドアには鍵が必要です。ドアに間違った鍵を使うと、そのドアは爆発してしまいがちです(ロボットの心配は要りません。壊れることはありませんが、焦げ跡をこすって落とすのはレスリーにとってかなり面倒です)。ロボットがどのドアも開けられる鍵を持っていなければ、部屋全体が爆発してしまいます!(これも心配は要りません。壊れはしませんが、掃除が面倒になります。)
レスリーを助けるために必要なのは、各部屋に合った正しい鍵をロボットに与えることです。ロボットは部屋の中の各ドアに順番に鍵を使い、最初に開いたドアを通り抜けます。
各テストは1つの迷路を表し、各アサーションは1つの部屋です。引数が等しいときはTに、等しくないときはNILに評価される述語関数を書く必要があります。
この迷路には部屋が1つしかありません。2つのオブジェクトが同じオブジェクトかどうかを見分けられる関数を用意する必要があります。
たとえば次のようになります。
a ; => "pizza"
b ; => "pizza"
(key-object-identity a a) ; => T
(key-object-identity a b) ; => NIL
aとbは同じに見えますが、実際には同じオブジェクトではないからです。
この迷路には部屋が2つあります。1つ目の部屋には、2つの数値が厳密に等しいかどうかを鍵で見分けられれば開くドアがあります。2つ目の部屋には、数値が等しいかどうかの確認に少し柔軟さを持たせた鍵が必要です。
たとえば次のようになります。
a ; => 13
b ; => 13.0
c ; => 13
(key-numbers a b) ; => NIL
(key-numbers a c) ; => T
(key-numbers-of-different-types a b) ; => T
aとbは数値としては同じでも、型が異なるからです。
この迷路には部屋が2つあります。1つ目の部屋には、2つの文字が厳密に等しいかどうかを鍵で見分けられれば開くドアがあります。2つ目の部屋には、文字を大文字と小文字を区別せずに比較できる鍵が必要です。
たとえば次のようになります。
a ; => #\X
b ; => #\x
c ; => #\X
(key-characters a b) ; => NIL
(key-characters a c) ; => T
(key-characters-case-insensitively a b) ; => T
2つの文字を比較するときに大文字と小文字を区別しないのは、とても寛容な等価述語だけだからです。
文字の迷路と同様に、1つ目の部屋には2つの文字列が等しいかどうかを確認する鍵が必要で、2つ目の部屋には大文字と小文字を区別しない比較ができる鍵が必要です。
たとえば次のようになります。
a ; => "pizza"
b ; => "PIZZA"
c ; => "pizza"
(key-strings a b) ; => NIL
(key-strings a c) ; => T
(key-strings-case-insensitively a b) ; => T
2つの文字列を比較するときに大文字と小文字を区別しないのは、とても寛容な等価述語だけだからです。
これは部屋の多い大きな迷路です。各部屋には、コンスが等しいものを_含んでいる_かどうかを確認する鍵が必要です。各部屋では、内容の異なるコンスの等価性を確認します。内容はシンボル、数値、文字などで、中にはそうした内容に対してより柔軟な等価性の定義を必要とする部屋もあります。
たとえば次のようになります。
syms-a ; => (left . right)
syms-b ; => (up . down)
syms-c ; => (left . right)
chars-a ; => (#\x . #\y)
chars-b ; => (#\x . #\Y)
chars-c ; => (#\x . #\y)
nums-a ; => (13 . 23)
nums-b ; => (13 . 23.0)
nums-c ; => (13 . 23)
(key-conses-of-symbols syms-a syms-b) ; => NIL
(key-conses-of-symbols syms-a syms-c) ; => T
(key-conses-of-characters chars-a chars-b) ; => NIL
(key-conses-of-characters chars-a chars-c) ; => T
(key-conses-of-characters-case-insensitively
chars-a chars-b) ; => T
(key-conses-of-numbers nums-a nums-b) ; => NIL
(key-conses-of-numbers nums-a nums-c) ; => T
(key-conses-of-numbers-of-different-types
nums-a nums-b) ; => T
コンスの内部まで見る等価述語であっても、比較するときに大文字と小文字や数値の型を無視できるほど寛容でないことがあるからです。
この迷路はもっと簡単で、部屋は2つだけです。 1つ目には、配列が同じ配列かどうかを確認する鍵が必要です。 2つ目には、配列の中身を確認する鍵が必要です。
たとえば次のようになります。
a ; => #[13 23]
b ; => #[13 23]
c ; => #[13 23.0]
(key-arrays a b) ; => NIL
(key-arrays a c) ; => NIL
(key-arrays-loosely a b) ; => T
(key-arrays-loosely a c) ; => T
配列の中身が等しいかどうかまで確認するのは、とても寛容な等価性チェッカーだけだからです。
Exercismに登録すれば、27個のコンセプト92個の演習、そして本物の人間によるメンタリングとともに、Common Lispを学んでマスターできます。すべて無料です。