PartialEqとEqを使えるように
はじめに
Rustで適当にクラスをつくって大小関係を評価したいときが多々あります。今の自分は適当に下のようにPartialEq
やOrd
をつけています。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
Rustを勉強する際にお世話になった『The Rust Programming Language』において、これらの説明は導出可能なトレイトに簡潔にまとめられています。あまりに簡潔なため、個人的にはもう少し説明が欲しいところです。実際、使用に際しては、よく分からないまま付けたり消したり、copilotの世話になってきました。この面倒を放置せずに、整理をするのがこの記事の目的です。「Eq
とPartialEq
って何が違うんだよ」と立ち止まり、貴重な時間を無駄にした記憶があるのは自分だけではないはずです。
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==b
とa!=b
の比較が可能」であることを意味します。基本的にA
とB
の型が一致する場合は実装されていて(自作クラスの場合でも実装は容易)、一部の型については異なる型についても比較可能になっています。比較可能な型については、PartialEqについてのdocumentに記載されています。
対称性および推移性というのは、次の性質のことです。
- 対称性:
a1 == a2
\Rightarrow a2 == a1
- 推移性:
a1 == a2
&&a2 == a3
\Rightarrow a1 == a3
どちらも当たり前な感じがしますが、異なる型の場合を考えてみると少し嬉しい感じがします。つまり
- 対称性: 型
A
,B
の値a
,b
について、PartialEq<B> for A
とPartialEq<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
の中身は空で、単に反射を満たすことを定義づけするトレイトになっています。
一応注意として、すべてのEq
はPartialEq
ですが、逆は必ずしも成り立ちません。その例として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