PartialEqとEqを使えるように

2024/04/30に公開

はじめに

Rustで適当にクラスをつくって大小関係を評価したいときが多々あります。今の自分は適当に下のようにPartialEqOrdをつけています。

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]

Rustを勉強する際にお世話になった『The Rust Programming Language』において、これらの説明は導出可能なトレイトに簡潔にまとめられています。あまりに簡潔なため、個人的にはもう少し説明が欲しいところです。実際、使用に際しては、よく分からないまま付けたり消したり、copilotの世話になってきました。この面倒を放置せずに、整理をするのがこの記事の目的です。「EqPartialEqって何が違うんだよ」と立ち止まり、貴重な時間を無駄にした記憶があるのは自分だけではないはずです。

PartialEq vs Eq

  • PartialEq: これは、2つの値が等しいかどうかを比較するためのトレイトです。==演算子をオーバーロードすることで、2つの値を比較する際に対称性および推移性を保証します。
  • Eq: これは、対称、推移に加えて、反射性を満たすeqメソッドを提供するトレイトです。Eqは、PartialEqが実装されている型に対してのみ実装できます。Eqが実装されている型は、==演算子での比較において等価性を保証します。

対称とか推移とかなんのこっちゃという感じがするかもしれません。使えるようになることが目的なため、理解を保留して一旦具体例を見ていきます。まず、例として次のコードを考えてみます。

struct MyStruct {
    val: u32,
}
fn main() {
    let a = MyStruct { val: 2 };
    let b = MyStruct { val: 2 };
    let c = MyStruct { val: 3 };
    println!("Are a and b equal? {}", a == b); 
    println!("Are a and c equal? {}", a == c); 
}

このコードはコンパイルできません。次のエラーの通り、PartialEqが実装されていないためです。

error[E0369]: binary operation `==` cannot be applied to type `MyStruct`
note: an implementation of `PartialEq` might be missing for `MyStruct`

PartialEqを実装して実行します。

#[derive(PartialEq)]
struct MyStruct {
    val: u32,
}
fn main() {
    let a = MyStruct { val: 2 };
    let b = MyStruct { val: 2 };
    let c = MyStruct { val: 3 };
    println!("Are a and b equal? {}", a == b); // true
    println!("Are a and c equal? {}", a == c); // false
}

うまく実行できました。特にエラーなく、a、b、cが等しいか否かを判定できています。これだけ見ると「Eqいらない...?」という気持ちになります。

Eqはコレクション型の比較や、標準ライブラリの関数で使われるようです。例えばHashSetを使った次のコードを考えてみます。

use std::collections::HashSet;

#[derive(PartialEq, Hash)]
struct MyStruct {
    val: u32,
}
fn main() {
    let mut hs = HashSet::new();
    hs.insert(MyStruct { val: 2 });
    hs.insert(MyStruct { val: 3 });
    println!("HashSet size: {}", hs.len()); // could not compile
}

このコードはコンパイルできません。以下のエラーが出てきました。

error[E0277]: the trait bound `MyStruct: Eq` is not satisfied

どうやらHashSetではEqトレイトを使用するため、これがない場合は比較ができないようです。次のコードのように、Eqを実装することで、HashSetがうまく機能するようになります。また、値が同じものは同じ扱いになり、直感的な挙動をすることが分かります。

use std::collections::HashSet;

#[derive(PartialEq, Eq, Hash)]
struct MyStruct {
    val: u32,
}

fn main() {
    let mut hs1 = HashSet::new();
    let mut hs2 = HashSet::new();
    hs1.insert(MyStruct { val: 2 });
    hs1.insert(MyStruct { val: 2 });
    hs2.insert(MyStruct { val: 2 });
    hs2.insert(MyStruct { val: 3 });
    println!("HashSet1 size: {}", hs1.len()); // 1
    println!("HashSet2 size: {}", hs2.len()); // 2
}

最初に

Eqは、PartialEqが実装されている型に対してのみ実装できます。

と書いたため、これについても確認しておきます。上のコードからPartialEqを削除します。

#[derive(Eq, Hash)]
struct MyStruct {
    val: u32,
}
// 以下省略

実行してみると次のエラー文が出てきます。予想通りで嬉しいですね。

error[E0277]: can't compare `MyStruct` with `MyStruct`
 --> src/bin/b.rs:4:10
  |
4 | #[derive(Eq, Hash)]
  |          ^^ no implementation for `MyStruct == MyStruct`
  |
  = help: the trait `PartialEq` is not implemented for `MyStruct`
note: required by a bound in `Eq`

ここまではderiveで付与してきましたが、自分で実装することも出来ます。例えば次の通りです。

#[derive(Debug)]
struct Pos {
    x: u32,
    y: u32,
}

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

impl Eq for Pos {}

fn main() {
    let pos1 = Pos { x: 1, y: 2 };
    let pos2 = Pos { x: 1, y: 2 };
    assert_eq!(pos1, pos2);  
}

ここまでの例から使用にあたっては次のことが言えそうです。

  • 単に値が一致するか確認したいならPartialEq
  • さらに一部のコレクション型を使うならば追加でEq

を実装すればよさそうです。この節のタイトルはPartialEq vs Eqですが、そもそも対立するものではありませんでした。以降ではもう少しちゃんと考えていきます。

PartialEq

最初に以下のように書きました。これについてもう少し丁寧に見ます。

  • PartialEq: これは、2つの値が等しいかどうかを比較するためのトレイトです。==演算子をオーバーロードすることで、2つの値を比較する際に対称性および推移性を保証します。

「2つの値が等しいかどうか」というのは、「型A,Bの値a,bについて、PartialEq<B> for Aが実装されている場合に、a==ba!=bの比較が可能」であることを意味します。基本的にABの型が一致する場合は実装されていて(自作クラスの場合でも実装は容易)、一部の型については異なる型についても比較可能になっています。比較可能な型については、PartialEqについてのdocumentに記載されています。

対称性および推移性というのは、次の性質のことです。

  • 対称性: a1 == a2 \Rightarrow a2 == a1
  • 推移性: a1 == a2 && a2 == a3 \Rightarrow a1 == a3

どちらも当たり前な感じがしますが、異なる型の場合を考えてみると少し嬉しい感じがします。つまり

  • 対称性: 型A,Bの値a,bについて、PartialEq<B> for APartialEq<A> for Bがあるとき、a == bならば b == aを満たす
  • 推移性: 型A,B,Cの値a,b,cについて、PartialEq<B> for A, PartialEq<A> for B, PartialEq<C> for Aがあるとき、a == b かつ b == c ならばa == c
    相互にPartialEqが実装されていれば異なる型同士でも実装できるということです。上の公式documentに

Note that the B: PartialEq<A> (symmetric) and A: PartialEq<C> (transitive) impls are not forced to exist, but these requirements apply whenever they do exist.

とあるように、必ずしも双方向に実装されているとは限らないようです。

Eq

次に、Eqについてもう少し詳しく見ていきましょう。最初に次のように書きました。

Eq: これは、対称、推移に加えて、反射性を満たすeqメソッドを提供するトレイトです。Eqは、PartialEqが実装されている型に対してのみ実装できます。

Eqトレイトは、PartialEqで見た対称性と推移性に加えて、反射性を満たすeqメソッドを提供します。反射性というのは、次の性質のことです。

  • 反射: a1 == a1

これは要するに、ある値が自分自身と等しいかどうかを確認するための方法です。Eqトレイトは、PartialEqトレイトを実装した型に対してのみ実装されます。==について考えている今、対称性が成り立っているときにわざわざ反射を明示する必要があるのか?という気がしますが、実際具体例で見せたようにEqの中身は空で、単に反射を満たすことを定義づけするトレイトになっています。

一応注意として、すべてのEqPartialEqですが、逆は必ずしも成り立ちません。その例としてf64が挙げられます。(f64は公式documentにあるようにPartialEqは実装されています。)

fn main() {
    let nan: f64 = std::f64::NAN;
    assert!(nan == nan);  //panic!
}

これのせいでf64関係のソートするとき一手間かかって嫌いです。

まとめ

ここまでの話をまとめると、次のようになります

  • PartialEqは、2つの値が等しいかどうかを比較するためのトレイトであり、==演算子をオーバーロードします。対称性と推移性を保証します。
  • Eqは、PartialEqを実装した型に対してのみ実装され、対称性、推移性に加えて反射性を満たすeqメソッドを提供します。Eqは、コレクション型の比較やライブラリ関数の使用など、特定の状況で使用されます。

これらのトレイトを適切に実装することで、Rustの型システムをより効果的に活用し、安全性と使いやすさを向上させることができるのかな?

Discussion