user> (= 2 (+ 1 1))
true
user> (= (str "fo" "od") "food")
true
このドキュメントでは、Clojureにおける等価性の概念について説明します。`=`、`==`、`identical?` といった関数と、それらがJavaの `equals` メソッドとどのように異なるかについて解説します。また、Clojureの `hash` と、それがJavaの `hashCode` とどのように異なるかについても説明します。このガイドの冒頭では、クイックリファレンスとして最も重要な情報をまとめ、その後、詳細なレビューを行います。
このガイドの情報は、特に明記されていない限り、Clojure 1.10.0 の動作を記述しています。
Clojureの `=` は、同じ値を表す不変値を比較する場合、または同一の可変オブジェクトを比較する場合にtrueになります。便宜上、`=` はJavaコレクション同士、またはClojureの不変コレクションとJavaコレクションを比較する場合にも、内容が等しければtrueを返します。ただし、Clojure以外のコレクションを使用する場合は、重要な注意点があります。
Clojureの `=` は、2つの不変のスカラー値で呼び出された場合、以下の場合にtrueになります。
両方の引数が `nil`、`true`、`false`、同じ文字、または同じ文字列(つまり、同じ文字シーケンス)である場合。
両方の引数がシンボル、またはキーワードであり、名前空間と名前が等しい場合。
両方の引数が同じ「カテゴリ」の数値であり、数値的に同じである場合。カテゴリは以下のいずれかです。
整数または比率
浮動小数点数(floatまたはdouble)
Clojureの `=` は、2つのコレクションで呼び出された場合、以下の場合にtrueになります。
両方の引数が *シーケンシャル*(シーケンス、リスト、ベクター、キュー、または `java.util.List` を実装するJavaコレクション)であり、`=` 要素が同じ順序で存在する場合。
両方の引数がセット(`java.util.Set` を実装するJavaセットを含む)であり、`=` 要素を持ち、順序を無視する場合。
両方の引数がマップ(`java.util.Map` を実装するJavaマップを含む)であり、`=` キー **と** 値を持ち、エントリの順序を無視する場合。
両方の引数が `defrecord` で作成されたレコードであり、`=` キー **と** 値を持ち、順序を無視し、**かつ** 同じ型を持つ場合。 `=` は、レコードとマップを比較する場合、キーと値に関係なく、型が異なるため `false` を返します。
Clojureの `=` は、2つの可変なClojureオブジェクト、つまりvar、ref、atom、またはagent、あるいは2つの「保留中」のClojureオブジェクト、つまりfuture、promise、またはdelayで呼び出された場合、以下の場合にtrueになります。
両方の引数が同一のオブジェクトである場合、つまり `(identical? x y)` がtrueである場合。
その他すべての型の場合
両方の引数が `deftype` で定義された同じ型である場合。型の `equiv` メソッドが呼び出され、その戻り値が `(= x y)` の値になります。
その他の型の場合は、Javaの `x.equals(y)` がtrueになります。
Clojureの `==` は、特に数値を対象としています。
`==` は、異なる数値カテゴリ(整数 `0` と浮動小数点数 `0.0` など)の数値で使用できます。
比較される値が数値でない場合は、例外がスローされます。
`=` または `==` を3つ以上の引数で呼び出すと、連続するすべてのペアが `=` または `==` の場合に、結果はtrueになります。 `hash` は、以下に示す例外を除き、`=` と一貫性があります。
例外、または予期しない動作
Clojureのハッシュベースのコレクション(マップキーまたはセット要素として)でClojure以外のコレクションを使用する場合、ハッシュ動作の違いにより、Clojureの対応するコレクションと同じとはみなされません。(等価性とハッシュ および CLJ-1372 を参照)
`=` を使用してコレクションを比較する場合、コレクション内の数値も `=` で比較されるため、上記の3つの数値カテゴリが重要になります。
「非数」値 `##NaN`、`Float/NaN`、および `Double/NaN` は、自分自身とさえ `=` または `==` になりません。 *推奨事項:* `=` を使用して互いに比較し、結果として `true` を取得したいClojureデータ構造内に `##NaN` を含めないでください。
0.0 は -0.0 と `=` です。
Clojureの正規表現、たとえば `#"a.*bc"` は、Javaの `java.util.regex.Pattern` オブジェクトを使用して実装されており、2つの `Pattern` オブジェクトに対するJavaの `equals` は、不変オブジェクトとしてドキュメント化されていますが、`(identical? re1 re2)` を返します。したがって、`(= #"abc" #"abc")` はfalseを返し、`=` は2つの正規表現がメモリ内でたまたま同じ同一オブジェクトである場合にのみtrueを返します。 *推奨事項:* `=` を使用して互いに比較し、正規表現インスタンスが同一オブジェクトでない場合でも `true` を取得したいClojureデータ構造内に正規表現インスタンスを使用しないでください。必要な場合は、最初に文字列に変換することを検討してください。たとえば、`(str #"abc")` → `"abc"` (CLJ-1182 を参照)
Clojureの永続キューは、`java.util.List` を実装するJavaコレクションとは、同じ順序で `=` 要素を持っている場合でも、`=` になりません(CLJ-1059 を参照)
`=` を使用してソートされたマップと別のマップを比較する場合、キーの型が異なる(たとえば、キーワードと数値)ため、`compare` がキーを比較するときに例外をスローすると、場合によっては例外がスローされます(CLJ-2325 を参照)
ほとんどの場合、`hash` は `=` と一貫性があります。つまり、`(= x y)` の場合、`(= (hash x) (hash y))` です。これが当てはまらない値またはオブジェクトの場合、Clojureのハッシュベースのコレクションはそれらの項目を正しく見つけたり削除したりできません。つまり、それらの項目を要素とするハッシュベースのセット、またはそれらの項目をキーとするハッシュベースのマップの場合です。
`hash` は、特別なfloat値とdouble値を除き、数値については `=` と一貫性があります。 *推奨事項:* `(double x)` を使用してfloatをdoubleに変換して、この問題を回避してください。
`hash` は、不変のClojureコレクションとそのClojure以外のコレクション counterparts については、`=` と一貫性がありません。詳細については、等価性とハッシュ セクションを参照してください。 *推奨事項:* Clojure以外のコレクションを他のClojureデータ構造に含める前に、Clojureの不変の counterparts に変換してください。
過去の情報:Clojure 1.10.2 で修正されるまで、`hash` はクラス `VecSeq` のオブジェクトについては `=` と一貫性がありませんでした。たとえば、`(seq (vector-of :int 0 1 2))` などの呼び出しから返されます(CLJ-1364 を参照)
Clojureにおける等価性は、ほとんどの場合 `=` を使用してテストされます。
user> (= 2 (+ 1 1))
true
user> (= (str "fo" "od") "food")
true
Javaの `equals` メソッドとは異なり、Clojureの `=` は、互いに同じ型を持たない多くの値に対してtrueを返します。
user> (= (float 314.0) (double 314.0))
true
user> (= 3 3N)
true
`=` は、2つの数値が同じ数値を持つ場合でも、**必ずしも** trueを返すとは限りません。
user> (= 2 2.0)
false
異なる数値カテゴリにわたって数値の等価性をテストする場合は、`==` を使用します。詳細については、以下の数値セクションを参照してください。
同じ順序で等しい要素を持つシーケンシャルコレクション(シーケンス、ベクター、リスト、およびキュー)は等しくなります。
user> (range 3)
(0 1 2)
user> (= [0 1 2] (range 3))
true
user> (= [0 1 2] '(0 1 2))
true
;; not = because different order
user> (= [0 1 2] [0 2 1])
false
;; not = because different number of elements
user> (= [0 1] [0 1 2])
false
;; not = because 2 and 2.0 are not =
user> (= '(0 1 2) '(0 1 2.0))
false
2つのセットは、等しい要素を持っている場合に等しくなります。セットは通常順序付けされていませんが、ソートされたセットであっても、等価性を比較する際にソート順序は考慮されません。
user> (def s1 #{1999 2001 3001})
#'user/s1
user> s1
#{2001 1999 3001}
user> (def s2 (sorted-set 1999 2001 3001))
#'user/s2
user> s2
#{1999 2001 3001}
user> (= s1 s2)
true
2つのマップは、同じキーのセットを持ち、各キーが各マップで等しい値にマップされている場合に等しくなります。セットと同様に、マップは順序付けされておらず、ソートされたマップのソート順序は考慮されません。
user> (def m1 (sorted-map-by > 3 -7 5 10 15 20))
#'user/m1
user> (def m2 {3 -7, 5 10, 15 20})
#'user/m2
user> m1
{15 20, 5 10, 3 -7}
user> m2
{3 -7, 5 10, 15 20}
user> (= m1 m2)
true
ベクターはインデックス付けされ、マップのような性質を持っていますが、マップとベクターはClojureでは `=` として比較されることはありません。
user> (def v1 ["a" "b" "c"])
#'user/v1
user> (def m1 {0 "a" 1 "b" 2 "c"})
#'user/m1
user> (v1 0)
"a"
user> (m1 0)
"a"
user> (= v1 m1)
false
Clojureコレクションに関連付けられているメタデータは、比較時に無視されます。
user> (def s1 (with-meta #{1 2 3} {:key1 "set 1"}))
#'user/s1
user> (def s2 (with-meta #{1 2 3} {:key1 "set 2 here"}))
#'user/s2
user> (binding [*print-meta* true] (pr-str s1))
"^{:key1 \"set 1\"} #{1 2 3}"
user> (binding [*print-meta* true] (pr-str s2))
"^{:key1 \"set 2 here\"} #{1 2 3}"
user> (= s1 s2)
true
user> (= (meta s1) (meta s2))
false
`defrecord` で作成されたレコードは、多くの点でClojureマップと同様に動作します。ただし、それらは同じ型の他のレコードにのみ `=` であり、それも同じキーと値を持っている場合のみです。同じキーと値を持っていても、マップと等しくなることはありません。
Clojureレコードを定義する場合、他の型と区別できる個別の型を作成するために行います。各型がClojureプロトコルとマルチメソッドで独自の動作を持つようにするためです。
user=> (defrecord MyRec1 [a b])
user.MyRec1
user=> (def r1 (->MyRec1 1 2))
#'user/r1
user=> r1
#user.MyRec1{:a 1, :b 2}
user=> (defrecord MyRec2 [a b])
user.MyRec2
user=> (def r2 (->MyRec2 1 2))
#'user/r2
user=> r2
#user.MyRec2{:a 1, :b 2}
user=> (def m1 {:a 1 :b 2})
#'user/m1
user=> (= r1 r2)
false ; r1 and r2 have different types
user=> (= r1 m1)
false ; r1 and m1 have different types
user=> (into {} r1)
{:a 1, :b 2} ; this is one way to "convert" a record to a map
user=> (= (into {} r1) m1)
true ; the resulting map is = to m1
Clojure `=` は、数値とClojureコレクションを除くすべての型について、Javaの `equals` と同じように動作します。
ブール値と文字は、等価性において単純です。
文字列もまた、Unicode を含むいくつかのケースを除いては単純です。Unicode 文字の異なるシーケンスから構成される文字列は、表示されたときに同じに見えることがあり、アプリケーションによっては、=
が false を返す場合でも等しいものとして扱う必要があります。興味があれば、Unicode equivalence の Wikipedia ページの「Normalization」を参照してください。この必要がある場合は、ICU (International Components for Unicode for Java) のようなライブラリが役立ちます。
2 つのシンボルは、名前空間とシンボル名が同じであれば等しいです。2 つのキーワードは、同じ条件であれば等しいです。Clojure は、キーワードの等価性テストを特に高速にします(単純なポインタ比較)。これは、Keyword クラスの intern
メソッドによって、同じ名前空間と名前を持つすべてのキーワードが同じキーワードオブジェクトを返すことが保証されることで実現されます。
Java の equals
は、2 つの数値の型と数値が同じ場合にのみ true になります。したがって、Integer 1 と Long 1 は型が異なるため、equals
は false になります。例外:Java の equals
は、数値的には等しいがスケールが異なる 2 つの BigDecimal 値に対しても false になります。たとえば、1.50M と 1.500M は等しくありません。この動作は、BigDecimal メソッド equals
のドキュメントに記載されています。
Clojure の =
は、「カテゴリ」と数値が同じであれば true になります。カテゴリは次のいずれかです。
整数または比率。整数には、Byte
、Short
、Integer
、Long
、BigInteger
、clojure.lang.BigInt
などのすべての Java 整数型が含まれ、比率は clojure.lang.Ratio
という名前の Java 型で表されます。
浮動小数点数:Float
と Double
10 進数:BigDecimal
したがって、(= (int 1) (long 1))
は、同じ整数カテゴリに属するため true ですが、(= 1 1.0)
は、異なるカテゴリ(整数と浮動小数点数)に属するため false です。整数と比率は Clojure の実装では別々の型ですが、=
の目的上は事実上同じカテゴリに属します。比率に対する算術演算の結果は、整数であれば自動的に整数に変換されます。したがって、Ratio 型の Clojure 数値はどの整数とも等しくなることはなく、比率と整数を比較すると、=
は常に正しい数値的解答 (false
) を返します。
Clojure には、数値の比較にのみ役立つ ==
もあります。=
が true を返すときはいつでも true を返します。また、数値が異なるカテゴリに属する場合でも、数値的に等しい場合は true を返します。したがって、(= 1 1.0)
は false ですが、(== 1 1.0)
は true です。
なぜ =
は数値に対して異なるカテゴリを持っているのか疑問に思うかもしれません。==
のように動作する場合、hash
を =
と一貫性を持たせることは困難(不可能ではないにしても)です(等価性とハッシュ のセクションを参照)。(float 1.5)
、(double 1.5)
、BigDecimal 値 1.50M、1.500M など、および比率 (/ 3 2)
のすべてに対して同じハッシュ値を返すことが保証されるように hash
を記述しようと想像してみてください。
Clojure は、値が集合の要素またはマップのキーとして使用される場合、等価性を比較するために =
を使用します。したがって、数値要素を持つ集合または数値キーを持つマップを使用する場合、Clojure の数値カテゴリが関係します。
浮動小数点値は、以前にその近似的な性質を学んでいない場合、驚くような動作をする可能性があることに注意してください。多くの場合、固定ビット数で表現されるため、多くの値を正確に表現できず、近似する必要がある(または範囲外になる)ため、近似値になります。これは、どのプログラミング言語の浮動小数点数にも当てはまります。
user> (def d1 (apply + (repeat 100 0.1)))
#'user/d1
user> d1
9.99999999999998
user> (== d1 10.0)
false
数値解析 と呼ばれる分野全体があり、数値近似を使用するアルゴリズムの研究に専念しています。浮動小数点演算の順序が注意深く作成されているため、近似解と正確な解の差を保証する Fortran コードのライブラリがあります。詳細を知りたい場合は、"What Every Computer Scientist Should Know About Floating-Point Arithmetic" を読むことをお勧めします。
少なくともある種の問題に対して正確な解答が必要な場合は、比率または BigDecimal がニーズに合う場合があります。これらは、必要な桁数が増加した場合(たとえば、多くの算術演算の後)、可変量のメモリと、大幅に長い計算時間を必要とすることに注意してください。また、円周率または 2 の平方根の正確な値が必要な場合は役に立ちません。
Clojure は、標準 IEEE 754 で定義された表現と動作を持つ、基盤となる Java の倍精度浮動小数点数(64 ビット)を使用します。特別な値 NaN
(「非数」)があり、これはそれ自体とさえ等しくありません。Clojure は、この値を記号値 ##NaN
として表します。
user> (Math/sqrt -1)
##NaN
user> (= ##NaN ##NaN)
false
user> (== ##NaN ##NaN)
false
この「値」がデータに現れると、奇妙な動作が発生します。##NaN
を集合要素またはマップのキーとして追加してもエラーは発生しませんが、検索して見つけることはできません。また、disj
や dissoc
などの関数を使用して削除することもできません。それを含むコレクションから作成されたシーケンスには、通常どおり表示されます。
user> (def s1 #{1.0 2.0 ##NaN})
#'user/s1
user> s1
#{2.0 1.0 ##NaN}
user> (s1 1.0)
1.0
user> (s1 1.5)
nil
user> (s1 ##NaN)
nil ; cannot find ##NaN in a set, because it is not = to itself
user> (disj s1 2.0)
#{1.0 ##NaN}
user> (disj s1 ##NaN)
#{2.0 1.0 ##NaN} ; ##NaN is still in the result!
多くの場合、##NaN
を含むコレクションは、(= ##NaN ##NaN)
が false
であるため、たとえそうあるべきように見えても、他のコレクションと =
になりません。
user> (= [1 ##NaN] [1 ##NaN])
false
奇妙なことに、##NaN
を含むコレクションが =
であるべきように見え、実際そうである例外があります。これは、(identical? ##NaN ##NaN)
が true
であるためです。
user> (def s2 #{##NaN 2.0 1.0})
#'user/s2
user> s2
#{2.0 1.0 ##NaN}
user> (= s1 s2)
true
Java の equals
メソッドには、浮動小数点値の特別なケースがあり、##NaN
をそれ自体と等しくします。Clojure の =
と ==
はそうではありません。
user> (.equals ##NaN ##NaN)
true
Java には、オブジェクトのペアの等価性を比較するための equals
があります。
Java には、この等価性の概念と一貫性のある(または少なくともそうあるべきだと文書化されている)hashCode
メソッドがあります。これは、equals
が true である 2 つのオブジェクト x
と y
について、x.hashCode()
と y.hashCode()
も等しいことを意味します。
このハッシュ一貫性プロパティにより、内部でハッシュ技術を使用するマップや集合などのハッシュベースのデータ構造を実装するために hashCode
を使用できます。たとえば、ハッシュテーブルを使用して集合を実装できます。異なる hashCode
値を持つオブジェクトは異なるハッシュバケットに配置でき、異なるハッシュバケット内のオブジェクトは互いに等しくなることはありません。
Clojure には、同様の理由で =
と hash
があります。Clojure の =
は、Java の equals
よりも多い数のオブジェクトのペアを互いに等しいと見なすため、Clojure の hash
は、より多くのオブジェクトのペアに対して同じハッシュ値を返す必要があります。たとえば、=
要素のシーケンスがシーケンス、ベクター、リスト、またはキューのいずれにあるかに関係なく、hash
は常に同じ値を返します。
user> (hash ["a" 5 :c])
1698166287
user> (hash (seq ["a" 5 :c]))
1698166287
user> (hash '("a" 5 :c))
1698166287
user> (hash (conj clojure.lang.PersistentQueue/EMPTY "a" 5 :c))
1698166287
ただし、Clojure の不変コレクションとその対応する非 Clojure コレクションを比較する場合、hash
は =
と一貫性がないため、2 つを混在させると、以下の例に示すように、望ましくない動作が発生する可能性があります。
user=> (def java-list (java.util.ArrayList. [1 2 3]))
#'user/java-list
user=> (def clj-vec [1 2 3])
#'user/clj-vec
;; They are =, even though they are different classes
user=> (= java-list clj-vec)
true
user=> (class java-list)
java.util.ArrayList
user=> (class clj-vec)
clojure.lang.PersistentVector
;; Their hash values are different, though.
user=> (hash java-list)
30817
user=> (hash clj-vec)
736442005
;; If java-list and clj-vec are put into collections that do not use
;; their hash values, like a vector or array-map, then those
;; collections will be equal, too.
user=> (= [java-list] [clj-vec])
true
user=> (class {java-list 5})
clojure.lang.PersistentArrayMap
user=> (= {java-list 5} {clj-vec 5})
true
user=> (assoc {} java-list 5 clj-vec 3)
{[1 2 3] 3}
;; However, if java-list and clj-vec are put into collections that do
;; use their hash values, like a hash-set, or a key in a hash-map,
;; then those collections will not be equal because of the different
;; hash values.
user=> (class (hash-map java-list 5))
clojure.lang.PersistentHashMap
user=> (= (hash-map java-list 5) (hash-map clj-vec 5))
false ; sorry, not true
user=> (= (hash-set java-list) (hash-set clj-vec))
false ; also not true
user=> (get (hash-map java-list 5) java-list)
5
user=> (get (hash-map java-list 5) clj-vec)
nil ; you were probably hoping for 5
user=> (conj #{} java-list clj-vec)
#{[1 2 3] [1 2 3]} ; you may have been expecting #{[1 2 3]}
user=> (hash-map java-list 5 clj-vec 3)
{[1 2 3] 5, [1 2 3] 3} ; I bet you wanted {[1 2 3] 3} instead
Clojure でマップを使用するほとんどの場合、配列マップまたはハッシュマップのどちらが必要かを指定しません。デフォルトでは、キーが最大 8 個の場合は配列マップが使用され、キーが 8 個を超える場合はハッシュマップが使用されます。マップに対する操作を実行すると、Clojure 関数が実装を選択します。したがって、配列マップを一貫して使用しようとした場合でも、より大きなマップを作成すると、頻繁にハッシュマップを取得する可能性があります。
Clojure でハッシュベースの集合とマップの使用を回避することは *お勧めしません*。ハッシュを使用して操作の高パフォーマンスを実現しています。代わりに、Clojure コレクション内に非 Clojure コレクションを使用することを避けることをお勧めします。主な理由は、ほとんどの非 Clojure コレクションは可変であり、可変性は微妙なバグにつながることが多いためです。もう 1 つの理由は、hash
と =
の不整合です。
同様の動作は、java.util.List
、java.util.Set
、および java.util.Map
を実装する Java コレクション、および Clojure の hash
が =
と一貫性のない少数の種類の値で発生します。
ハッシュと非整合の値を *任意の* Clojure コレクション内で、リストやベクターなどの順次コレクションの要素として使用した場合でも、それらのコレクションも互いにハッシュと非整合になります。これは、コレクションのハッシュ値がその部分のハッシュ値を組み合わせて計算されるために発生します。
非 Clojure コレクションで hash
が =
と一貫性がない *理由* が疑問に思われるかもしれません。非 Clojure コレクションは、Clojure が存在するずっと前から Java の hashCode
メソッドを使用していました。Clojure が最初に開発されたとき、コレクション要素からハッシュ関数を計算するために hashCode
と同じ式を使用していました。
Clojure 1.6.0 のリリース前に、Clojure の hash
関数に hashCode
を使用すると、小さなコレクションが集合要素またはマップキーとして使用される場合に多くのハッシュ衝突が発生する可能性があることが発見されました。
たとえば、[0, 99] の範囲の 2 つの数値のベクターであるキーを持つマップを使用して、100 行 100 列の 2 次元グリッドの内容を表す Clojure プログラムを考えてみましょう。このグリッドには 10,000 個のポイントがあるため、マップには 10,000 個のキーがありますが、hashCode
は 3,169 個の異なる結果しか返しません。
user=> (def grid-keys (for [x (range 100), y (range 100)]
[x y]))
#'user/grid-keys
user=> (count grid-keys)
10000
user=> (take 5 grid-keys)
([0 0] [0 1] [0 2] [0 3] [0 4])
user=> (take-last 5 grid-keys)
([99 95] [99 96] [99 97] [99 98] [99 99])
user=> (count (group-by #(.hashCode %) grid-keys))
3169
したがって、マップがハッシュマップのデフォルトの Clojure 実装を使用する場合、ハッシュバケットあたり平均 10,000 / 3,169 = 3.16 の衝突が発生します。
Clojure の開発者は、いくつかの代替ハッシュ関数を分析し、Murmur3 ハッシュ関数に基づく関数を選択しました。これは Clojure 1.6.0 以降使用されています。また、コレクション内の複数の要素のハッシュを組み合わせるために、Java の hashCode
とは異なる方法を使用します。
当時、Clojure は非 Clojure コレクションにも新しい技術を使用するように hash
を変更できましたが、そうすると hash
の実装に使用される hasheq
と呼ばれる Java メソッドが大幅に遅くなると判断されました。これまでに検討されてきたアプローチについては、CLJ-1372 を参照してください。しかし、現時点では、競争力のある高速な方法を発見した人はいません。
hash
が =
と一貫性のないその他のケース互いに =
である一部の Float 値と Double 値では、それらの hash
値は一貫性がありません。
user> (= (float 1.0e9) (double 1.0e9))
true
user> (map hash [(float 1.0e9) (double 1.0e9)])
(1315859240 1104006501)
user> (hash-map (float 1.0e9) :float-one (double 1.0e9) :oops)
{1.0E9 :oops, 1.0E9 :float-one}
浮動小数点コードで一貫して一方の型を使用することにより、Float
対 Double
のハッシュの不整合を回避できます。Clojure は浮動小数点値のデフォルトで double を使用するため、それが最も便利な選択肢かもしれません。
これを行う方法の例などについては、以下のプロジェクトのコードを参照してください。特に、標準 Java オブジェクトの Java メソッド equals
と hashCode
、および Clojure Java メソッド equiv
と hasheq
は、=
と hash
の動作に最も関連しています。
org.flatland/ordered ですが、カスタム順序マップデータ構造がClojureレコードと=
にならないように変更する必要があります: PR #42
Henry Bakerによる論文"関数オブジェクトの平等、あるいは、変化すればするほど、同じになる"("Equal Rights for Functional Objects, or, the More Things Change, The More They Are the Same")には、Clojureの=
のインスピレーションとなった関数EGAL
のCommon Lispで書かれたコードが含まれています。不変値には「深い等価性」が意味を持つが、可変オブジェクトには(可変オブジェクトがメモリ内で同じオブジェクトでない限り)それほど意味を持たないという考え方は、プログラミング言語に依存しません。
EGAL
とClojureの=
の違いを以下に説明します。これらはEGAL
の動作に関するかなり難解な詳細であり、Clojureの=
を理解するために知る必要はありません。
EGAL
は、可変オブジェクトを他のものと比較する場合、その他のものがメモリ内で同じ可変オブジェクトでない限り、false
と定義されています。
便宜上、Clojureの=
は、Clojureの不変コレクションを非Clojureコレクションと比較する場合、いくつかのケースでtrue
を返すように設計されています。
任意のコレクションが可変か不変かを判断するJavaメソッドはないため、ClojureではEGAL
の意図した動作を実装することはできません。ただし、引数の1つが非Clojureコレクションである場合に常にfalse
を返す場合、=
はEGAL
に「近い」と考えることができます。
Bakerは、EGAL
が遅延値を比較する際に強制的に評価することを推奨しています(「関数オブジェクトの平等」論文のセクション3. J.「遅延値」を参照)。遅延シーケンスを別のシーケンスと比較する場合、Clojureの=
は遅延シーケンスの評価を強制し、=
でないシーケンス要素に達すると停止します。range
によって生成されるようなチャンクシーケンスは、Clojureで遅延シーケンスの一部の評価を引き起こすイベントの場合と同様に、評価をそのポイントより少し आगे 進める可能性があります。
Clojureの=
は、遅延、promise、またはfutureオブジェクトを比較する際にderef
しません。代わりに、identical?
を介して比較するため、それらにderef
を呼び出すと=
となる値が得られる場合でも、メモリ内で同じオブジェクトである場合にのみtrue
を返します。
Bakerは、EGAL
がクロージャを互いに比較する場合、いくつかのケースでtrue
を返すことができる方法を詳細に説明しています(「関数オブジェクトの平等」論文のセクション3. D.「関数と関数クロージャの等価性」を参照)。
関数またはクロージャを引数として指定すると、Clojureの=
は、それらが互いにidentical?
である場合にのみtrue
を返します。
BakerがEGAL
をこのように定義しようとしたのは、一部のLisp系言語では、可変状態または不変値を含むことができるオブジェクトを表すためにクロージャを使用することが一般的だったためです(以下の例を参照)。Clojureには、不変値と可変オブジェクトを作成する他の方法が複数あるため(例:レコード、reify、プロキシ、deftype)、クロージャを使用してこれを行うことは一般的ではありません。
(defn make-point [init-x init-y]
(let [x init-x
y init-y]
(fn [msg]
(cond (= msg :get-x) x
(= msg :get-y) y
(= msg :get-both) [x y]
:else nil))))
user=> (def p1 (make-point 5 7))
#'user/p1
user=> (def p2 (make-point -3 4))
#'user/p2
user=> (p1 :get-x)
5
user=> (p2 :get-both)
[-3 4]
user=> (= p1 p2)
false ; We expect this to be false,
; because p1 and p2 have different x, y values
user=> (def p3 (make-point 5 7))
#'user/p3
user=> (= p1 p3)
false ; Baker's EGAL would return true here. Clojure
; = returns false because p1 and p3 are not identical?
原著者: Andy Fingerhut