🦀

【Rust】PartialEq/EqとPartialOrd/Ordを理解する

に公開

今回はRustのトレイトPartialEqEqPartialOrdOrdについて。

これらは値の比較に用いられるトレイトですが、それぞれPartialとそうでないトレイトに分かれています。これらの違いをあまり理解していない方や、なんとなく理解はしているものの説明することは難しい、という方も多いのではないでしょうか。

そこで今回の記事では、これらのトレイトの基本的な使い方から、なぜわざわざトレイトが分かれているか、などについても解説していきます。

基本の使い方

まずはPartialEq/Eq/PartialOrd/Ordの使い方をおさらいしておきましょう。

詳しい違いは後ほど説明しますが、PartialEq/Eq==!=による値の等値比較を実装したいとき、PartialOrd/Ord<>=による値の大小比較を実装したいときに利用できます。

例として、usizeをラップするだけのId構造体に対して、これらのトレイトを実装してみましょう。

struct Id(usize);

impl PartialEq for Id {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

// EqはPartialEqの実装を要求するが、追加の実装は要求しない
impl Eq for Id {}

impl PartialOrd for Id {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.0.partial_cmp(&other.0)
    }
}

// OrdはPartialOrdの実装を要求するが、追加の実装は要求しない
impl Ord for Id {}

これにより、Idの値の比較を行えるようになります。

fn main() {
    let a = Id(1);
    let b = Id(2);

    println!("{}", a == b); // false
    println!("{}", a != b); // true
    println!("{}", a < b);  // true
    println!("{}", a >= b); // false
}

また、これらはメンバー全てがそれぞれのトレイトを実装している場合に限り、#[derive]マクロから導出することが可能です。

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Id(usize);

これだけを見ると、Eq/Ordは追加の実装を持たないため、わざわざ存在する意味がわからないかもしれません。しかし、これらのトレイトがわざわざ二つに分かれているのには明確な理由があります。

二項関係

本題の前に、まずは前提知識となる数学の話を済ませましょう。

ある集合 A を考えます。この集合の2つの元 x, y \in Aに対して x = yx < y のような何らかの関係が成り立つとき、これを 二項関係 と呼び、x, yの間の二項関係はRを用いて xRy のように書くことができます。

ここでRは、集合Aの元のうち関係が成り立つペアを列挙した集合です。例えば

A = \{1, 2, 3\}

とすると、値が等しいことを表す二項関係R

R = \{ (1, 1), (2, 2), (3, 3) \}

となるでしょう。つまり、Rは集合Aの直積集合A \times Aの部分集合

R \subseteq A \times A

として表すことができます。

重要な4つの関係

二項関係には以下の4つの重要な関係が存在します。順番に見ていきましょう。

わかりやすさのために、命題論理による定義をRustっぽく書き直した擬似コードを載せています。

反射律 (Reflexive Relation)

\forall x \in A, xRx

任意の x \in A に対して xRx が成り立つとき、Rは反射律 を満たすと言います。

x => x.r(x)

推移律 (Transitive Relation)

\forall x \in A, \forall y \in A, \forall z \in A, xRy, xRz \Rightarrow xRz

任意のx, y, z \in Aに対して xRy かつ yRz ならば xRz が成り立つとき、Rは推移律を満たすと言います。

x.r(y) && y.r(z) => x.r(z)

対称律 (Symmetric Relation)

\forall x \in A, \forall y \in A, xRy \Rightarrow yRx

任意のx, y\in Aに対して xRy ならば yRx が成り立つとき、Rは対称律を満たすと言います。

x.r(y) => y.r(x)

反対称律 (Antisymmetric Relation)

\forall x \in A, \forall y \in A, xRy, yRx \Rightarrow x = y

任意のx, y\in Aに対して xRy かつ yRx ならばx = yが成り立つとき、Rは反対称律を満たすと言います。

x.r(y) && y.r(x) => x == y

同値関係・半同値関係

前置きが長くなりましたが、ようやくPartialEq/Eqの話です。

先ほどの関係を用いると、同値関係 x = yは 「反射律」「推移律」「対称律」を満たす二項関係 と定義することができます。この同値関係(Equivalence Relation)がEqトレイトに相当します。

一方、「推移律」「対称律」を満たす二項関係は半同値関係(Partial Equivalence Relation) と呼ばれます。一見同値関係と同じように思えますが、半同値関係は「反射律」が保証されていません。これがPartialEqトレイトに相当します。

PartialEqトレイトを実装するがEqを実装しない(半同値関係が成り立つが同値関係が成り立たない)型の典型的な例は浮動小数点数型(f32など)です。これは浮動小数点数のNaNNaN != NaNであると定義されており、反射律が成り立たないためです。

fn main() {
    // NAN != NAN
    assert_ne!(f32::NAN, f32::NAN);
}

全順序・半順序

同様にPartialOrd/Ordも見ていきましょう。

全順序は 「反射律」「推移律」「反対称律」に加え、以下の「完全律」を満たす二項関係 と定義できます。

完全律:
\forall x \in A, \forall y \in A, x \leq y \space\text{or}\space x \geq y

任意のx, y\in Aに対して x \leq y または x \geq y が成り立つとき、Rは完全律を満たすと言います。

この全順序(Total Order)がOrdトレイトに相当します。

一方、 半順序(Partial Order)は「反射律「推移律」「反対称律」を満たしますが、「完全律」を満たしません。 PartialOrdが実装するpartial_cmp()Option<Ordering>を返すのはこれが理由で、比較できないペアに対してNoneを許容するためです。

PartialOrdトレイトを実装するがOrdを実装しない(半順序が成り立つが全順序が成り立たない)型の典型的な例もやはり浮動小数点数型です。こちらも NaN同士の比較が定義されていないことで、完全律が成り立たなくなる からです。

実用上の使い分け

それでは、実際にこれらのトレイトの違いを理解して使う必要があるケースは何でしょうか。

おそらく一番困るのが、そのままでは浮動小数点数型のソートができないことでしょう。f32などの型はOrdを実装していないため、そのままではソートができず、partial_cmp()unwrap()するなどして対応する必要があります。

fn main() {
    let mut v = vec![1.2, 3.4, 0.5];

    // これはコンパイルエラー
    v.sort(); 

    // これはOK、ただしNaNが入り込むとpanicを起こす
    v.sort_by(|a, b| a.partial_cmp(b).unwrap());
}

同様の理由で、浮動小数点数型をBTreeMapのキーとして利用することはできません。

これはやや不便ですが、NaNが入り込むことによる不具合をコンパイル時点で回避するための制約とも言えます。

まとめ

というわけで、PartialEq/Eq/PartialOrd/Ordトレイトについての解説でした。普段はあまり違いについて意識することはないかもしれませんが、小数周りで引っかかることが時々あるので、覚えておくと良いでしょう。

Discussion