Clojure

コンパレータガイド

注:このドキュメントはClojure 1.10とJava 8について説明していますが、他のほとんどのバージョンにも適用されます。

概要

コンパレータとは、2つの引数 *x* と *y* を受け取り、*x* と *y* をソートする際の相対的な順序を示す値を返す関数です。 整数を返す3ウェイコンパレータ、または真偽値を返す2ウェイコンパレータがあります。 *x* と *y* の順序に応じて、戻り値がどうなるべきかについては、以下の「すべきこと」を参照してください。

Clojureでは、値のコレクションをソートしたり、値のコレクションを目的のソート順に維持したりするためにコンパレータが必要です。たとえば、sorted-mapsorted-set、またはpriority-map(優先度キューとも呼ばれます)などです。

デフォルトのコンパレータcompareは、数値を昇順でソートしたり、文字列、キーワード、またはシンボルを辞書順でソートしたりする場合に適しています。その他いくつかのケースにも適しています。例と詳細については、以下を参照してください。

compareが期待どおりに動作しない場合は、期待どおりに動作する独自のコンパレータを提供する必要があります。以下の推奨事項はそれぞれ、このドキュメントの後半で詳しく説明されています。

すべきこと

  • コンパレータが、比較する値の全順序に基づいていることを確認してください。データセットに現れる可能性のある任意の値のペアを比較し、どちらの値が先に来るか(または等しいか)を判断できる必要があります。

  • 3ウェイコンパレータまたはブール値コンパレータのいずれかを記述します

    • 3ウェイコンパレータは、2つの値 *x* と *y* を受け取り、*x* が *y* より前に来る場合は負、*x* が *y* より後に来る場合は正、等しい場合は0のJava 32ビット *int* を返します。他の戻り値を優先する理由がない場合は、値-1、0、1を使用します。

    • ブール値コンパレータは、2つの値 *x* と *y* を受け取り、*x* が *y* より前に来る場合はtrueを返し、そうでない場合はfalseを返します(*x* と *y* が等しい場合を含む)。<>が良い例です。<=>=は良くありません。パフォーマンスに関する注意:ブール値コンパレータは、「後」または「等しい」のケースを区別するために2回呼び出される場合があります。

  • 既存のコンパレータに引数を指定する順序を逆にすることで、ソートを逆転します。

  • 「ソートキー」を含む等しい長さのClojureベクトルを比較して、値間の複数フィールド比較を実行します。

  • コレクションをソートする前に、データから「非数」(##NaN)の出現を削除または置換し、ソートされたコレクションのキーの一部として使用しないでください。

すべきでないこと

  • 値が等しい場合にtrueを返すブール値コンパレータを作成しないでください。このようなコンパレータは一貫性がありません。ソートされたコレクションが正しく動作しなくなり、ソートで予測できない順序が生成されます。

  • ソートされたコレクションにこれらの2つの値の最大1つだけを表示する場合を除き、2つの値を等しいとみなすコンパレータをソートされたセットとマップに使用しないでください。

  • 自分が何をしているかを本当に理解していない限り、3ウェイコンパレータを記述するときに減算を使用しないでください。

はじめに

ここでは、関数 `compare` によって提供されるデフォルトのソート順について説明します。その後、他のコンパレータの例を示し、独自のコンパレータを作成する際に従うべきガイドラインと避けるべき間違いについて説明します。

Clojureのデフォルトコンパレータ

独自のコンパレータを指定しない場合、ソートは組み込み関数 `compare` によって行われます。 `compare` は多くのタイプの値に対して機能し、特定の方法で順序付けます

  • 数値は、`==` によって数値的に等しい場合は0を返すように、数値の昇順でソートされます。`=` がfalseを返す場合でもです。例外:すべての数値 *x*(`##NaN` も含む)に対して `(== ##NaN x)` はfalseですが、すべての数値 *x*(`##NaN` も含む)に対して `(compare ##NaN x)` は0です。

  • 文字列は、UTF-16コード単位のシーケンスとしての表現によって、辞書順(辞書順)でソートされます。これは、ASCIIサブセットに制限された文字列の場合はアルファベット順(大文字と小文字を区別)です。

  • シンボルは、最初に名前空間(存在する場合)でソートされ、同じ名前空間を持つ場合は名前でソートされます。名前空間と名前はどちらも、文字列表現が辞書順に比較されます。名前空間を持たないすべてのシンボルは、名前空間を持つシンボルの前にソートされます。

  • キーワードはシンボルと同じ方法でソートされますが、キーワードとシンボルを比較しようとすると例外がスローされます。

  • ベクトルは、要素数が少ないものから多いものの順にソートされ、等しい長さのベクトル間では辞書順でソートされます。

  • Clojure refは、作成された順序でソートされます。

  • 文字、ブール値、File、URI、UUIDなど、Comparableインターフェースを実装するすべてのJava型は、`compareTo`メソッドを介して比較されます。

  • `nil`:上記のすべての値と比較でき、他の何よりも小さいと見なされます。

`compare`は、型の「違いが大きすぎる」2つの値(たとえば、整数、long、doubleを互いに比較できますが、文字列とキーワード、またはキーワードとシンボルは比較できません)が指定された場合に例外をスローします。リスト、シーケンス、セット、またはマップを比較することはできません。

以下の `sort`、`sorted-set`、`sorted-map` の例はすべて、デフォルトのコンパレータを使用しています。

user> (sort [22/7 2.71828 ##-Inf 1 55 3N])
(##-Inf 1 2.71828 3N 22/7 55)

user> (sorted-set "aardvark" "boo" "a" "Antelope" "bar")
#{"Antelope" "a" "aardvark" "bar" "boo"}

user> (sorted-set 'user/foo 'clojure.core/pprint 'bar 'clojure.core/apply 'user/zz)
#{bar clojure.core/apply clojure.core/pprint user/foo user/zz}

user> (sorted-map :map-key 10, :amp [3 2 1], :blammo "kaboom")
{:amp [3 2 1], :blammo "kaboom", :map-key 10}

user> (sort [[-8 2 5] [-5 -1 20] [1 2] [1 -5] [10000]])
([10000] [1 -5] [1 2] [-8 2 5] [-5 -1 20])

user> (import '(java.util UUID))
java.util.UUID

user> (sort [(UUID. 0xa 0) (UUID. 5 0x11) (UUID. 5 0xb)])
(#uuid "00000000-0000-0005-0000-00000000000b"
 #uuid "00000000-0000-0005-0000-000000000011"
 #uuid "00000000-0000-000a-0000-000000000000")

user> (sort [:ns2/kw1 :ns2/kw2 :ns1/kw2 :kw2 nil])
(nil :kw2 :ns1/kw2 :ns2/kw1 :ns2/kw2)

異なる型で `compare` を呼び出すと、例外がスローされます。上記の任意の数値型は互いに比較できますが、数値以外の型とは比較できません。リスト、セット、マップ、または上記以外の型で `compare` を使用した場合も、例外がスローされます。このような値をソートする場合は、独自のコンパレータを実装する必要があります。

既製のコンパレータ

まず、特に複雑な場合は、他の人が開発した十分にテストされたコンパレータの使用を検討してください。

この完璧な例は、異なるロケールに固有の順序で、異なる言語のUnicode文字列をソートすることです。JavaのCollatorクラスとICU(International Components for Unicode)は、このためのライブラリを提供しています。

独自のコンパレータを作成する

逆順

数値を降順でソートするには、`compare` を逆の順序で引数に呼び出すコンパレータを作成するだけです

user> (sort [4 2 3 1])
(1 2 3 4)

user> (defn reverse-cmp [a b]
        (compare b a))
#'user/reverse-cmp

user> (sort reverse-cmp [4 3 2 1])
(4 3 2 1)

このような短い関数は、多くの場合、Clojureの#()表記を使用して記述されます。2つの引数は、その順序で%1と%2です。

user> (sort #(compare %2 %1) [4 3 2 1])

`reverse-cmp` は、`compare` が機能する他のすべての型でも機能します。

複数フィールドコンパレータ

等しい長さのClojureベクトルは辞書順に比較されるため、マップやレコードなどの値の複数フィールドソートに使用できます。これは、フィールドがすでに希望する順序(またはその逆)で `compare` によってソートされている場合にのみ機能します。

最初に、ベクトルを比較しない方法を示します。

(def john1 {:name "John", :salary 35000.00, :company "Acme"})
(def mary  {:name "Mary", :salary 35000.00, :company "Mars Inc"})
(def john2 {:name "John", :salary 40000.00, :company "Venus Co"})
(def john3 {:name "John", :salary 30000.00, :company "Asteroids-R-Us"})
(def people [john1 mary john2 john3])

(defn by-salary-name-co [x y]
  ;; :salary values sorted in decreasing order because x and y
  ;; swapped in this compare.
  (let [c (compare (:salary y) (:salary x))]
    (if (not= c 0)
      c
      ;; :name and :company are sorted in increasing order
      (let [c (compare (:name x) (:name y))]
        (if (not= c 0)
          c
          (let [c (compare (:company x) (:company y))]
            c))))))

user> (pprint (sort by-salary-name-co people))
({:name "John", :salary 40000.0, :company "Venus Co"}
 {:name "John", :salary 35000.0, :company "Acme"}
 {:name "Mary", :salary 35000.0, :company "Mars Inc"}
 {:name "John", :salary 30000.0, :company "Asteroids-R-Us"})

以下は、Clojureベクトルを比較することで、より短い方法です。上記とまったく同じように動作します。上記と同様に、:salaryフィールドは、* x *と * y *がスワップされているため、降順にソートされます。

(defn by-salary-name-co2 [x y]
    (compare [(:salary y) (:name x) (:company x)]
             [(:salary x) (:name y) (:company y)]))

user> (pprint (sort by-salary-name-co2 people))
({:name "John", :salary 40000.0, :company "Venus Co"}
 {:name "John", :salary 35000.0, :company "Acme"}
 {:name "Mary", :salary 35000.0, :company "Mars Inc"}
 {:name "John", :salary 30000.0, :company "Asteroids-R-Us"})

上記は、ソートされる値から計算するのに費用がかからないキー値に適しています。キー値の計算に費用がかかる場合は、値ごとに1回計算する方が効果的です。sort-byのドキュメントに記載されている「decorate-sort-undecorate」手法を参照してください。

ブール値コンパレータ

Javaのコンパレータはすべて3ウェイであり、最初の引数が2番目の引数よりも小さい、等しい、または大きいと見なされるべきかどうかによって、負の整数、0、または正の整数を返します。

Clojureでは、最初の引数が2番目の引数の前に来るべき場合は`true`を返し、そうでない場合は`false`(つまり、後になるか、等しい)を返すブールコンパレータを使用することもできます。関数`<`は、数値を比較するだけであれば最適な例です。`>`は、数値を降順でソートするために機能します。このようなClojure関数`bool-cmp-fn`が「コンパレータとして呼び出される」と、背後では、Clojureは次のように動作するコードを実行して、代わりに_int_を返します。

(if (bool-cmp-fn x y)
  -1     ; x < y
  (if (bool-cmp-fn y x)  ; note the reversed argument order
    1    ; x > y
    0))  ; x = y

これは、任意のClojure関数のcompareメソッドを呼び出すことで確認できます。以下は、呼び出されたときに引数を出力する`<`のカスタムバージョン`my-<`の例です。これにより、複数回呼び出される場合を確認できます。

user> (defn my-< [a b]
        (println "(my-<" a b ") returns " (< a b))
        (< a b))
#'user/my-<

;; (. o (compare a b)) calls the method named compare for object
;; o, with arguments a and b.  In this case the object is the
;; Clojure function my-<
user> (. my-< (compare 1 2))
(my-< 1 2 ) returns  true
-1
user> (. my-< (compare 2 1))
(my-< 2 1 ) returns  false
(my-< 1 2 ) returns  true
1
user> (. my-< (compare 1 1))
(my-< 1 1 ) returns  false
(my-< 1 1 ) returns  false
0

;; Calling a Clojure function in the normal way uses its invoke
;; method, not compare.
user> (. my-< (invoke 2 1))
(my-< 2 1 ) returns  false
false

詳細については、Clojureソースファイルsrc/jvm/clojure/lang/AFunction.javaのメソッド`compare`を参照してください。

コンパレータの一般的なルール

3ウェイかブールかにかかわらず、コンパレータは、比較する値の全順序と一致する答えを返す必要があります。

全順序とは、すべての値を最小から最大に順序付けしたもので、値のグループがすべて互いに等しい場合があります。すべての値のペアは互いに比較可能でなければなりません(つまり、コンパレータから「比較方法がわかりません」という回答が返されることはありません)。

たとえば、整数mとnに対して_m/n_の形式で記述されたすべての分数を、数学で行われる通常の方法で最小から最大に順序付けることができます。多くの分数は互いに等しくなります。たとえば、_1/2 = 2/4 = 3/6_です。その全順序を実装するコンパレータは、それらがすべて同じであるかのように動作する必要があります。

3ウェイコンパレータ`(cmp a b)`は、_a_が全順序でbより前、後、またはbと等しいと見なされる場合、それぞれ負、正、または0の_int_を返す必要があります。

ブールコンパレータ`(cmp a b)`は、_a_が全順序で_b_より前の場合はtrueを返し、_a_が_b_より後または_b_と等しいと見なされる場合はfalseを返す必要があります。つまり、数値に対する`<`のように動作する必要があります。後述するように、数値に対する`<=`のように動作するべきではありません(「ソートされたセットとマップのコンパレータは間違えやすい」セクションを参照)。

避けるべき間違い

ソートされたコレクションで比較される値として「非数」値に注意してください

Clojureのデフォルトのコンパレータ`compare`は、「非数」(`##NaN`)値を他のすべての数値と等しいと見なします。`##NaN`が出現する数値のシーケンスに対してsortを呼び出すと、例外がスローされる可能性があります。

user> (sort [##NaN 5 13 ##NaN 3 7 12 ##NaN 8 4 2 20 6 9 ##NaN 50 83 19 -7 0 18 26 30 42 ##NaN 57 90 -8 -12 43 87 38])
Execution error (IllegalArgumentException) at java.util.TimSort/mergeHi (TimSort.java:899).
Comparison method violates its general contract!

例外がスローされない場合でも、返されたシーケンスがソートされない可能性があります。これは、`compare`が`sort`が正しく機能するために必要なコンパレータのように、`##NaN`を他の数値との全順序に配置しないためです。

user> (sort [##NaN 10 5 13 ##NaN 3 7 12 ##NaN 8 4 2 20 6 9 ##NaN 50 83 19 -7])
(##NaN -7 2 3 4 5 6 7 8 10 12 13 ##NaN ##NaN 9 19 20 ##NaN 50 83)

`##NaN`は他のどの値とも等しくないため、次のようなコードを使用して数値のシーケンスから値を削除することはできません。

user> (remove #(= % ##NaN) [9 3 ##NaN 4])
(9 3 ##NaN 4)

関数`NaN?`を使用して、値が`##NaN`かどうかを判断できます。関数`NaN?`はClojureバージョン1.11.0で追加されました。Javaメソッド`Double/isNaN`は、どのバージョンのClojureでも使用できます。

user> (remove NaN? [9 3 ##NaN 4])
(9 3 4)
user> (remove #(Double/isNaN %) [9 3 ##NaN 4])
(9 3 4)

ソートされたセットとマップのコンパレータは間違えやすい

これは「コンパレータは間違えやすい」と述べるのと同様に正確ですが、ソートされたセットとマップに間違ったコンパレータを使用すると、より顕著になることがよくあります。このセクションのような間違ったコンパレータを作成して`sort`を呼び出すと、通常はほとんど、またはまったく問題が発生しません(ただし、一貫性のないコンパレータはソートにも適していません)。ソートされたセットとマップでは、これらの間違ったコンパレータによって、値がソートされたコレクションに追加されないか、追加されても検索時に見つからない可能性があります。

それぞれが文字列と数値のベクトルである2つの要素のベクトルを含むソートされたセットが必要だとします。たとえば、`["a" 5]`です。数値でソートされたセットが必要で、同じ数値で異なる文字列を持つ複数のベクトルを許可します。最初の試みでは、`by-2nd`のようなものを書くかもしれません。

(defn by-2nd [a b]
  (compare (second a) (second b)))

しかし、同じ番号の複数のベクトルを追加しようとするとどうなるかを見てください。

user> (sorted-set-by by-2nd ["a" 1] ["b" 1] ["c" 1])
#{["a" 1]}

`by-2nd`は3つのベクトルすべてを等しいと見なすため、セットには1つの要素しかありません。セットには重複する要素を含めるべきではないため、他の要素は追加されません。

このような場合の一般的な考え方は、`<`ではなく`<=`に基づくブールコンパレータ関数を使用することです。

(defn by-2nd-<= [a b]
  (<= (second a) (second b)))

ブールコンパレータ`by-2nd-<=`は、セットの作成の最初のステップでは正しく機能しているように見えますが、要素がセットにあるかどうかをテストすると失敗します。

user> (def sset (sorted-set-by by-2nd-<= ["a" 1] ["b" 1] ["c" 1]))
#'user/sset
user> sset
#{["c" 1] ["b" 1] ["a" 1]}
user> (sset ["c" 1])
nil
user> (sset ["b" 1])
nil
user> (sset ["a" 1])
nil

ここでの問題は、`by-2nd-<=`が一貫性のない答えを与えることです。`["c" 1]`が`["b" 1]`の前に来るかどうかを尋ねると、trueを返します(Clojureのブール値から整数へのコンパレータ変換では-1になります)。`["b" 1]`が`["c" 1]`の前に来るかどうかを尋ねると、再びtrueを返します(再びClojureによって-1に変換されます)。一貫性のないコンパレータを与えると、ソートされたデータ構造の実装がその動作に対して何らかの保証を提供することを合理的に期待することはできません。

上記の「複数フィールドのコンパレータ」で説明されている手法は、この例に対して正しいコンパレータを提供します。一般に、値の一部のみを互いに比較することに注意してください。対象となるすべてのフィールドを比較した後、何らかのタイブレーク条件を設けることを検討してください。

補足:セットに同じ番号の複数のベクトルを含めたくない場合は、`by-2nd`が使用するコンパレータです。それはあなたが望むとおりの動作をします。(TBD:ここに注意事項はありますか?`sorted-set`は何らかの理由で要素を比較するために`=`を使用しますか、それとも指定されたコンパレータ関数のみを使用しますか?)

コンパレータで減算を使用する場合の注意

Javaコンパレータは、最初の引数が2番目の引数よりも小さいと見なされる場合は負のint値を、最初の引数が2番目の引数よりも大きいと見なされる場合は正のint値を、等しい場合は0を返します。

user> (compare 10 20)
-1
user> (compare 20 10)
1
user> (compare 20 20)
0

このため、次のように、数値を別の数値から減算することでコンパレータを作成したくなるかもしれません。

user> (sort #(- %1 %2) [4 2 3 1])
(1 2 3 4)

これは多くの場合に機能しますが、この手法を使用する前によく考えてください(2、3回)。明示的な条件チェックを使用して-1、0、または1を返すか、ブールコンパレータを使用する方がエラーが発生しにくいです。

なぜでしょうか?Javaコンパレータは32ビットの_int_型を返す必要があるため、Clojure関数がコンパレータとして使用され、任意のタイプの数値を返すと、その数値はJavaメソッドintValueを使用して背後で_int_に変換されます。詳細については、Clojureソースファイルsrc/jvm/clojure/lang/AFunction.javaのメソッド`compare`を参照してください。

浮動小数点数と比率を比較する場合、-1から1の間の戻り値は_int_ 0に切り捨てられるため、1未満の差がある数値は等しいと見なされます。

;; This gives the correct answer
user> (sort #(- %1 %2) [10.0 9.0 8.0 7.0])
(7.0 8.0 9.0 10.0)

;; but this does not, because all values are treated as equal by
;; the bad comparator.
user> (sort #(- %1 %2) [1.0 0.9 0.8 0.7])
(1.0 0.9 0.8 0.7)

;; .intValue converts all values between -1.0 and 1.0 to 0
user> (map #(.intValue %) [-1.0 -0.99 -0.1 0.1 0.99 1.0])
(-1 0 0 0 0 1)

これは、32ビットの_int_に切り捨てる(最下位32ビット以外をすべて破棄する)と符号が変わる量だけ異なる整数値を比較する場合にもバグにつながります。long値のペアの約半分は、コンパレータとして減算を使用すると誤って比較されます。

;; This looks good
user> (sort #(- %1 %2) [4 2 3 1])
(1 2 3 4)

;; What the heck?
user> (sort #(- %1 %2) [2147483650 2147483651 2147483652 4 2 3 1])
(3 4 2147483650 2147483651 2147483652 1 2)

user> [Integer/MIN_VALUE Integer/MAX_VALUE]
[-2147483648 2147483647]

;; How .intValue truncates a few selected values.  Note especially
;; the first and last ones.
user> (map #(.intValue %) [-2147483649 -2147483648 -1 0 1
                            2147483647  2147483648])
(2147483647 -2147483648 -1 0 1 2147483647 -2147483648)

Java itself uses a subtraction comparator for strings and characters, among others. This does not cause any problems, because the result of subtracting an arbitrary pair of 16-bit characters converted to ints is guaranteed to fit within an _int_ without wrapping around. If your comparator is not guaranteed to be given such restricted inputs, better not to risk it.文字列や文字などを比較するために、Java自体では減算コンパレータが使用されます。これは問題を引き起こしません。intに変換された任意の16ビット文字のペアを減算した結果は、ラップアラウンドなしで_int_に収まることが保証されているためです。コンパレータにそのような制限された入力が与えられることが保証されていない場合は、リスクを冒さない方がよいでしょう。

異なる型間で機能するコンパレータ

場合によっては、コレクションの値をあるキーでソートしたいが、そのキーが一意ではない場合があります。同じキーを持つ値を予測可能で反復可能な順序でソートしたいが、その順序はあまり気にしません。

おもちゃの例として、それぞれに2つの要素を持つベクトルのコレクションがあり、最初の要素は常に文字列で、2番目の要素は常に数値です。数値で昇順にソートしたいが、データに同じ数値のベクトルが複数含まれている可能性があることがわかっています。複数のソートで一貫して、何らかの方法でタイを解消したいと考えています。

この場合は、前のセクションで説明した複数フィールドのコンパレータを使用して簡単に実装できます。

(defn by-number-then-string [[a-str a-num] [b-str b-num]]
  (compare [a-num a-str]
           [b-num b-str]))

すべてのベクトルが同じ長さであり、対応する各要素の型を`compare`で互いに比較できるため、ベクトル値全体を`compare`で比較できる場合は、ベクトル値全体を最終的なタイブレーカーとして使用して、これを行うこともできます。

(defn by-number-then-whatever [a-vec b-vec]
  (compare [(second a-vec) a-vec]
           [(second b-vec) b-vec]))

ただし、ベクトルの要素の位置に`compare`が機能するには違いすぎる型が含まれており、それらのベクトルに2番目の要素が同じである場合、例外がスローされます。

;; compare throws exception if you try to compare a string and a
;; keyword
user> (sort by-number-then-whatever [["a" 2] ["c" 3] [:b 2]])
Execution error (ClassCastException) at user/by-number-then-whatever (REPL:2).
class java.lang.String cannot be cast to class clojure.lang.Keyword

以下の`cc-cmp`(「クロス クラス比較」)は、そのような場合に役立つ可能性があります。値の型を表す文字列に基づいて順序付けられる、異なる型の値を比較できます。`(class x)`だけではありません。そうすると、`Integer`や`Long`などの数値は数値順にソートされません。ライブラリclj-arrangementも役立つ場合があります。

;; comparison-class throws exceptions for some types that might be
;; useful to include.

(defn comparison-class [x]
  (cond (nil? x) ""
        ;; Lump all numbers together since Clojure's compare can
        ;; compare them all to each other sensibly.
        (number? x) "java.lang.Number"

        ;; sequential? includes lists, conses, vectors, and seqs of
        ;; just about any collection, although it is recommended not
        ;; to use this to compare seqs of unordered collections like
        ;; sets or maps (vectors should be OK).  This should be
        ;; everything we would want to compare using cmp-seq-lexi
        ;; below.  TBD: Does it leave anything out?  Include anything
        ;; it should not?
        (sequential? x) "clojure.lang.Sequential"

        (set? x) "clojure.lang.IPersistentSet"
        (map? x) "clojure.lang.IPersistentMap"
        (.isArray (class x)) "java.util.Arrays"

        ;; Comparable includes Boolean, Character, String, Clojure
        ;; refs, and many others.
        (instance? Comparable x) (.getName (class x))
        :else (throw
               (ex-info (format "cc-cmp does not implement comparison of values with class %s"
                                (.getName (class x)))
                        {:value x}))))

(defn cmp-seq-lexi
  [cmpf x y]
  (loop [x x
         y y]
    (if (seq x)
      (if (seq y)
        (let [c (cmpf (first x) (first y))]
          (if (zero? c)
            (recur (rest x) (rest y))
            c))
        ;; else we reached end of y first, so x > y
        1)
      (if (seq y)
        ;; we reached end of x first, so x < y
        -1
        ;; Sequences contain same elements.  x = y
        0))))

;; The same result can be obtained by calling cmp-seq-lexi on two
;; vectors, but cmp-vec-lexi should allocate less memory comparing
;; vectors.
(defn cmp-vec-lexi
  [cmpf x y]
  (let [x-len (count x)
        y-len (count y)
        len (min x-len y-len)]
    (loop [i 0]
      (if (== i len)
        ;; If all elements 0..(len-1) are same, shorter vector comes
        ;; first.
        (compare x-len y-len)
        (let [c (cmpf (x i) (y i))]
          (if (zero? c)
            (recur (inc i))
            c))))))

(defn cmp-array-lexi
  [cmpf x y]
  (let [x-len (alength x)
        y-len (alength y)
        len (min x-len y-len)]
    (loop [i 0]
      (if (== i len)
        ;; If all elements 0..(len-1) are same, shorter array comes
        ;; first.
        (compare x-len y-len)
        (let [c (cmpf (aget x i) (aget y i))]
          (if (zero? c)
            (recur (inc i))
            c))))))


(defn cc-cmp
  [x y]
  (let [x-cls (comparison-class x)
        y-cls (comparison-class y)
        c (compare x-cls y-cls)]
    (cond (not= c 0) c  ; different classes

          ;; Compare sets to each other as sequences, with elements in
          ;; sorted order.
          (= x-cls "clojure.lang.IPersistentSet")
          (cmp-seq-lexi cc-cmp (sort cc-cmp x) (sort cc-cmp y))

          ;; Compare maps to each other as sequences of [key val]
          ;; pairs, with pairs in order sorted by key.
          (= x-cls "clojure.lang.IPersistentMap")
          (cmp-seq-lexi cc-cmp
                        (sort-by key cc-cmp (seq x))
                        (sort-by key cc-cmp (seq y)))

          (= x-cls "java.util.Arrays")
          (cmp-array-lexi cc-cmp x y)

          ;; Make a special check for two vectors, since cmp-vec-lexi
          ;; should allocate less memory comparing them than
          ;; cmp-seq-lexi.  Both here and for comparing sequences, we
          ;; must use cc-cmp recursively on the elements, because if
          ;; we used compare we would lose the ability to compare
          ;; elements with different types.
          (and (vector? x) (vector? y)) (cmp-vec-lexi cc-cmp x y)

          ;; This will compare any two sequences, if they are not both
          ;; vectors, e.g. a vector and a list will be compared here.
          (= x-cls "clojure.lang.Sequential")
          (cmp-seq-lexi cc-cmp x y)

          :else (compare x y))))

`cc-cmp`が異なる型の値を比較できることを示す簡単な例を次に示します。

user> (pprint (sort cc-cmp [true false nil Double/MAX_VALUE 10
                            Integer/MIN_VALUE :a "b" 'c (ref 5)
                            [5 4 3] '(5 4) (seq [5]) (cons 6 '(1))
                            #{1 2 3} #{2 1}
                            {:a 1, :b 2} {:a 1, :b -2}
                            (object-array [1 2 3 4])]))
(nil
 {:a 1, :b -2}
 {:a 1, :b 2}
 #{1 2}
 #{1 2 3}
 :a
 #<Ref@1493d9b3: 5>
 (5)
 (5 4)
 [5 4 3]
 (6 1)
 c
 false
 true
 -2147483648
 10
 1.7976931348623157E308
 "b"
 [1, 2, 3, 4])
nil

原著者:Andy Fingerhut