- Common Lisp には
eqeqlequal=equalpと複数の同値比較関数があり、何を「同じ」とみなすかで使い分ける。 eqはメモリ上の同一オブジェクトか、eqlは型も含めた値の一致、equalは文字列やリストを再帰的に値で比較する。- 文字列をリスト検索やハッシュテーブルのキーに使うときは
:test #'equalを明示しないと、デフォルトのeqlでは一致しないバグになる。
1. 基盤となる同値比較関数(= equal eql)
Common Lisp には「同じか?」を調べる関数が複数あります。
何を「同じ」とみなすかで使い分けが変わり、それぞれ適用できる型も異なります。
(defvar str "abc")
(= (length str) 3) ;=> T
(= str "abc") ;=> type error
(eql str "abc") ;=> NIL
(equal str "abc") ;=> T Code language: Lisp (lisp)
| やりたいこと | 使う関数 |
|---|---|
| 数値の比較(型区別なし) | = |
| リスト・文字列を値で比較 | equal |
| 数値の比較(型区別あり) | eql |
| シンボルの比較 | eq |
計算が軽いものから、範囲が広いものの順に並べると、
eq < eql < = < equal
型ごとの比較、=char、=string。
1.1. = (数値が同じ)
=は、数値が同じか比較します。
(= 3 3) ; => T
(= 1 2) ; => NIL
(= (+ 1 2 3) (* 2 3)) ; => TCode language: Lisp (lisp)
42 と 42.0 は T になるように、型をまたいで数学的な等価性を確認する点に特徴があります1。
(= 42 42.0) ; => T 型をまたいで比較
(= 0.5 1/2) ; => T 型をまたいで比較
(= 1 1 2) ; => nil 複数引数も取れる
(/= 2 4) ; => TCode language: PHP (php)
反対は、/=です。
また、(= 1 1 1) のように複数引数を渡すと 連鎖比較できます。
ただし、=に文字列や他の型を渡すと型エラーになります。
1.2. equal (中身が同じ)
リストや文字列などでも値が一致するかを調べるには、equalを使います。
構造を再帰的にたどってくれるので、日常的なデータ比較のほとんどはこれで済みます。
(equal '(1 2 3) '(1 2 3)) ; => T
(equal "abc" "abc") ; => T 大小区別あり
(equal #(1 2) #(1 2)) ; => NIL ベクタは eq で比較される
(equal 42 42.0) ; => NIL 型は区別するCode language: Lisp (lisp)
「印字表現が同じオブジェクトは equal が T を返す」というのが実用上の目安です2。
ただし、ベクタには注意が必要です。
各要素は基本的には eq で比較されるため(文字列とビットベクタを除く)、内容が同じでも別オブジェクトなら NIL になります3。
equalの中身を再実装するなら、かなり複雑な処理をして「同一性」を確認します。
(defun my-equal (x y)
(or (eq x y)
;; 数値は型も含めて比較(= だけでは 42 と 42.0 が区別できない)
(and (numberp x) (numberp y)
(= x y)
(eq (type-of x) (type-of y)))
;; 文字
(and (characterp x) (characterp y)
(= (char-code x) (char-code y)))
;; 文字列を文字コードで逐次比較
(and (stringp x) (stringp y)
(= (length x) (length y))
(every (lambda (a b) (= (char-code a) (char-code b)))
x y))
;; リストを再帰的にたどる
(and (consp x) (consp y)
(my-equal (car x) (car y))
(my-equal (cdr x) (cdr y)))
;; ベクタ
(and (vectorp x) (vectorp y)
(= (length x) (length y))
(every #'my-equal x y))))Code language: Lisp (lisp)
このため、循環リストを渡すと終了しない場合があります4。
1.3. eql (デフォルトの値比較)
Common Lispでの「デフォルト」の比較関数は、= ではありません。
リスト内の同じ要素を探すときに、よく使われているのは eqlです。
eqlは、 = よりも厳しい値の比較です。
数値と文字に対応し、比較する2つの型も同じ必要があります。
(eql 42 42) ; => T
(eql 42 42.0) ; => NIL 型が違う
(eql #\a #\a) ; => T
(eql "a" "a") ; => NIL 文字列はオブジェクトが違うCode language: Lisp (lisp)
eqlの動作をmy-eqlとして再実装すると、
(defun my-eql (x y)
(or (eq x y)
(and (numberp x) (numberp y)
(= x y)
(eq (type-of x) (type-of y)))
(and (characterp x) (characterp y)
(= (char-code x) (char-code y)))))Code language: Lisp (lisp)
eqlは、複素数の比較にも使えますが、実部・虚部の型もチェックします。。
つまり、(eql #C(4 5) #C(4 5)) は T ですが、(eql #C(4 5) #C(4.0 5.0)) は NIL です5。
2. eq (メモリ上のオブジェクトが同じ)
eqlやequalよりも、基礎的な同値判定関数に、eq があります。
eq は、シンボルやリストの比較で使われ「同じメモリ上のオブジェクトか」を確認します。
(defvar a '(1 2 3))
(defvar b a)
(defvar c '(1 2 3))
(eq a b) ; => T
(eq a c) ; => NILCode language: Lisp (lisp)
a と b は、同じリストオブジェクトですが、c は内容は同じようでも別のリストです。
処理系の最深部にある操作で、C で書かれた処理系ではオブジェクトのポインタの == に直結します。
2.1. 文字列の比較とシンボルの比較
この違いは、文字列やシンボルでもあります。
文字列変数は、書き換えができるようにするため別オブジェクトが生成されます。
一方、シンボルは、同じオブジェクトとして返されます。
(defvar str "abc")
(eq str "abc") ;=> NIL
(equal str "abc") ;=> T
; 現在のパッケージのシンボル(例: COMMON-LISP-USER::ABC)
(defvar sym 'abc)
(eq sym 'abc) ;=> T
; KEYWORD パッケージのシンボル
(defvar kw :abc)
(eq kw :abc) ;=> T
(eq sym kw) ;=> NILCode language: Lisp (lisp)
データの種別を表すには:clickや:keydownのようなキーワードを使うのが Common Lisp の慣用です。
一文字ずつ判定する文字列より、シンボルで判定する方が効率的だからです。
(defparameter *events*
'((:type :click :x 10 :y 20)
(:type :keydown :key :enter)
(:type :click :x 50 :y 80)))
(defun process-events (events)
(dolist (ev events)
(let ((type (getf ev :type)))
(cond
((eq type :click)
(format t "click at ~A,~A~%" (getf ev :x) (getf ev :y)))
((eq type :keydown)
(format t "key: ~A~%" (getf ev :key)))))))Code language: Lisp (lisp)
getfは、プロパティリストからキーに対応する値を取り出す関数です。
'abc と :abc の主な違いは、属するパッケージです。:typeは、どこから参照しても同じオブジェクトです。
しかし、'abc'は、書いた場所のパッケージに属すので、別パッケージのコードから参照してeq で比較すると、NIL になります。
ただ、文字列リテラルは、コンパイル時に同一オブジェクトに最適化される可能性もあり、(eq "Foo" "Foo") がインタプリタとコンパイル済みコードで異なる結果を返すことがあるからです6。
2.2. :test で同値比較関数を選ぶ
リストのmember やハッシュテーブルのキーなどは、数値やシンボルのキー向けのeql が使われます。
文字列のリストを検索するときは明示的に :test #'equal を指定しないと、文字列が見つからないバグになります。
(member "b" '("a" "b" "c"))
; => NIL eql では文字列を比較できない
(member "b" '("a" "b" "c") :test #'equal)
; => ("b" "c")Code language: Lisp (lisp)
make-hash-table でも、文字列をキーにするなら :test #'equal を指定します。
(make-hash-table) ; eql — シンボルや数値のキー向け
(make-hash-table :test #'equal) ; 文字列キーに必要Code language: Lisp (lisp)
これを忘れると gethash が常に NIL を返します7。
2.3. 数値とオブジェクト
数値は処理系によって同一オブジェクトになるかが変わります8。
(eq 1 1)は、実装依存で、手元のSBCLでは、数値はそのままオブジェクトではなく値で比較されました。
(defvar x 3)
(defvar y 3)
(eq x 3) ;=> T
(eq 3 3) ;=> T
(eq x y) ;=> TCode language: PHP (php)
3. 文字の同値判定関数
3.1. char= / char-equal — 文字専用
文字は、char= で文字コードで比較します。
これは、eqlでも代用できます。
(char= #\a #\a) ; => T 大小区別
(defun my-char= (a b)
(= (char-code a) (char-code b)))Code language: Lisp (lisp)
基本的には、文字列操作の実装の内側で使う場面がほとんどです。
(char-equal #\A #\a) ; => T 大小無視
(defun my-char-equal (a b)
(= (char-code (char-upcase a))
(char-code (char-upcase b))))Code language: Lisp (lisp)
大文字・小文字の違いを無視する、char-equal もあります。
大小の違いだけでなく、実装依存の「その他の文字属性」も無視することがあります9。
3.2. string= / string-equal — 文字列専用
文字列の比較では、string=を使います。
これは、equal でも代用できます。
ただ、文字列以外を受け取ると型エラーにできるため、より意図が明確になります。
(string= "abc" "abc") ; => T 大小区別
(defun my-string= (s1 s2)
(and (= (length s1) (length s2))
(every (lambda (a b) (= (char-code a) (char-code b)))
s1 s2)))Code language: Lisp (lisp)
string<、string>、string<=、string>= で辞書順比較もできます。
また、大小を無視する変種には -equal サフィックスが付きます。
(string-equal "ABC" "abc") ; => T 大小無視
(string< "abc" "abd") ; => 3 不一致位置のインデックスを返すCode language: Lisp (lisp)
3.3. 部分文字列の比較(string= :start1 :end1 :start2 :end2)
ちなみに、string= と string-equal はともに部分文字列を比較するキーワード引数 :start1、:end1、:start2、:end2 を受け取れます10。
4. equalp — 緩やかな等価性
ハッシュテーブルでは、大文字・小文字の区別をしないときには、equalpを使います。
(make-hash-table :test #'equalp) ; 大小無視のキー
(equalp "ABC" "abc") ; => T
(equalp 42 42.0) ; => T
(equalp #(1 2) #(1 2)) ; => T
(defun my-equalp (x y)
(or (my-equal x y)
;; 数値は型をまたいで比較
(and (numberp x) (numberp y) (= x y))
;; 文字は大小無視
(and (characterp x) (characterp y)
(= (char-code (char-upcase x))
(char-code (char-upcase y))))
;; 文字列は大小無視
(and (stringp x) (stringp y)
(my-string-equal x y))
;; ベクタは要素を equalp で再帰
(and (vectorp x) (vectorp y)
(= (length x) (length y))
(every #'my-equalp x y))))Code language: Lisp (lisp)
ただし、equal より広い範囲を一致とみなすため、意図せず一致してしまうケースが増えます。equalp は defstruct で定義した構造体のスロットを再帰的に比較しますが、defclass で定義したインスタンスには降りてきません11。
=に有理数を渡すこともできます。(= 2 4/2)はTを返します。有理数は Common Lisp の組み込み数値型で、整数の比を正確に表現します。 – The Common Lisp Cookbook – Equality- 仕様書には “a rough rule of thumb is that two objects are equal if and only if their printed representations are the same” と記述されています。 – CLHS: Function EQUAL
equalが要素を再帰的に比較する対象は、cons セル、文字列、ビットベクタ、パス名に限られます。一般のベクタや配列、ハッシュテーブル、構造体はeqで比較されます。 – CLHS: Function EQUAL- 仕様上、
equalに循環構造を渡した場合は終了しないことがあると明記されています。循環リストの比較には別途対策が必要です。 – CLHS: Function EQUAL - 複素数は実部と虚部がそれぞれ
eqlであるときにeqlとみなされます。また、実装によっては浮動小数点のフォーマット数が仕様の4種類より少ない場合があり、その場合(eql 1.0s0 1.0d0)がTになることがあります。 – CLHS: Function EQL - コンパイラはソースコード中の同一の定数(
quoteに含まれるオブジェクトや自己評価フォーム)を同一オブジェクトに折りたたむことが許可されています。インタプリタでは(eq "Foo" "Foo")は通常NILですが、コンパイル済みではTになる場合があります。 – CLtL2: 6.3 Equality Predicates - 標準では
:testに指定できるのはeq、eql、equal、equalpの4つです。独自の比較関数を使いたい場合は処理系拡張か Alexandria などのライブラリが必要です。 – The Common Lisp Cookbook – Equality - Common Lisp の仕様では、処理系がパフォーマンス向上のために数値や文字のコピーを作ることを許可しています。そのため
(eq 1 1)がTになるかどうかは処理系依存です。 – CLHS: Function EQ - 仕様では
char-equalは “ignores alphabetic case and certain other attributes of characters” と定義されています。どの属性を無視するかは処理系依存です。 – CLHS: Function EQUALP - たとえば
(string= "abcdef" "bcd" :start1 1 :end1 4)はTを返します。この引数はstring<などの順序比較関数でも共通して使えます。 – The Common Lisp Cookbook – Strings equalpが降りていく対象はコンス、ビットベクタ、文字列、配列、構造体、ハッシュテーブルで、CLOSのクラスインスタンスはeqで比較されます。構造体と異なりクラスインスタンスにequalpが使えない点は設計上の非一貫性として知られています。 – Equality in Common Lisp