PartialOrdとOrdを使えるように
はじめに
Rustで適当にクラスをつくって大小関係を評価したいときが多々あります。その際に使われるPartialEq
とEq
については、以前に自分が分かるように整理しました。
前の記事はPartialEq
とEq
について、今後時間を割かなく良いようにすることが目的でした。この記事はOrd
とPartialOrd
についても同様に、今後ググったりしなくて済むようにすることが目的です。
これらのトレイトは、型の比較に関する挙動を制御し、コレクション型のソートにおいて重要な役割を果たしていそうなことが分かっています。でも実際何やっているんだ、というところを見ていきます。
PartialOrd vs Ord
-
PartialOrd
: これは、2つの値を比較し、その大小関係を判定するためのトレイトです。<
、>
、<=
、>=
演算子をオーバーロードすることで、2つの値の大小を比較する際に、反射、反対称、推移を満たすことを保証します。 -
Ord
: これは、PartialOrd
が実装されている型に対してのみ実装できるトレイトです。Ord
は、cmp
メソッドを提供し、完全な順序付けを行うことができます。
反射律、反対称律、推移律という概念が分からないかもしれません。使えるようになることが目的なので、一旦具体例を見ていきましょう。まず、例として次のコードを考えてみます。
struct MyStruct {
val: u32,
}
fn main() {
let a = MyStruct { val: 2 };
let b = MyStruct { val: 3 };
println!("Is a less than b? {}", a < b);
}
このコードはコンパイルできません。次のエラーの通り、PartialOrd
が実装されていないためです。
error[E0369]: binary operation `<` cannot be applied to type `MyStruct`
note: an implementation of `PartialOrd` might be missing for `MyStruct`
PartialOrd
を実装して実行します。
#[derive(Debug, Clone, PartialOrd)]
struct MyStruct {
val: u32,
}
//以下略
まだコンパイルできません。次のエラーが出てきました。
= help: the trait `PartialEq` is not implemented for `MyStruct`
note: required by a bound in `PartialOrd`
どうやらPartialOrd
を実装するためにはPartialEq
も実装する必要があるようです。PartialEq
を実装してみます。
#[derive(Debug, Clone, PartialOrd, PartialEq)]
struct MyStruct {
val: u32,
}
fn main() {
let a = MyStruct { val: 2 };
let b = MyStruct { val: 3 };
println!("Is a less than b? {}", a < b);
}
うまく実行できました。特にエラーなく、a、bの大小を判定できています。これだけ見ると「Ord
はいつ使うんだ...?」という気持ちになります。
Eq
はソートやコレクション型の比較の際に使われるものでした。同様に、Ord
もソートやコレクション型の比較に使われます。例えば`単純にソートを行う次のコードを考えてみます。
#[derive(Clone, Debug, PartialOrd, PartialEq)]
struct MyStruct {
val: u32,
}
fn main() {
let mut v = vec![
MyStruct { val: 2 },
MyStruct { val: 3 },
MyStruct { val: 1 },
];
v.sort();
println!("Sorted: {:?}", v);
}
このコードはコンパイルできません。以下のエラーが出てきました。
error[E0277]: the trait bound `MyStruct: Ord` is not satisfied
どうやらsort
ではOrd
トレイトを使用するため、これがない場合は正しくソートができないようです。Ord
を実装しましょう。
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq)]
struct MyStruct {
val: u32,
}
// 以下省略
まだコンパイルできません。次のエラーが出てきました。
error[E0277]: the trait bound `MyStruct: Eq` is not satisfied
note: required by a bound in `Ord`
PartialOrd
がPartialEq
を必要としていたのと同様に、Ord
もEq
を必要としているようです。Eq
を実装してみます。
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
struct MyStruct {
val: u32,
}
fn main() {
let mut v = vec![
MyStruct { val: 2 },
MyStruct { val: 3 },
MyStruct { val: 1 },
];
v.sort();
println!("Sorted: {:?}", v); // Sorted: [MyStruct { val: 1 }, MyStruct { val: 2 }, MyStruct { val: 3 }]
}
うまく動きました。Ord
にはEq
が必要であることと、PartialOrd
とOrd
を実装することで、ソートができることが分かりました。
最初に
Ord
は、PartialOrd
が実装されている型に対してのみ実装できます。
と書いたため、これについても確認しておきます。上のコードからPartialOrd
を削除します。
#[derive(Ord, PartialEq, Eq, Ord)]
struct MyStruct {
val: u32,
}
// 以下省略
実行してみると次のエラー文が出てきます。予想通りOrd
がPartialOrd
を要求しています。完全に予想通りで嬉しいですね。
error[E0277]: can't compare `MyStruct` with `MyStruct`
--> src/bin/c.rs:1:28
|
1 | #[derive(Clone, Debug, Eq, Ord, PartialEq)]
| ^^^ no implementation for `MyStruct < MyStruct` and `MyStruct > MyStruct`
|
= help: the trait `PartialOrd` is not implemented for `MyStruct`
note: required by a bound in `Ord`
ここまではderiveで付与してきましたが、自分で実装することも出来ます。例えば次の通りです。
#[derive(Debug, Clone)]
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 {}
impl PartialOrd for Pos {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Pos {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.y
.cmp(&other.y)
.then_with(|| self.x.cmp(&other.x))
}
}
fn main() {
let pos1 = Pos { x: 1, y: 2 };
let pos2 = Pos { x: 1, y: 4 };
let pos3 = Pos { x: 2, y: 4 };
assert!(pos1 <= pos2);
assert!(pos1 < pos3);
}
ここまでの例から使用にあたっては次のことが言えそうです。
- 単に値の大小を比較したいなら
PartialOrd
- さらにソートやコレクション型を使うならば追加で
Ord
を実装すればよさそうです。この節のタイトルはPartialOrd vs Ord
ですが、そもそも対立するものではありませんでした。自分で実装する際に、PartialEq
は帰り値がboolであるのに対し、PartialOrd
はOption<Ordering>であるなど、まだ分からないところがあります。次の節でそれについても触れていきます。
Partial Ord
先の節では、PartialOrd
について次のように説明しました。
PartialOrd
: これは、2つの値を比較し、その大小関係を判定するためのトレイトです。<
、>
、<=
、>=
演算子をオーバーロードすることで、2つの値の大小を比較する際に、反射、反対称、推移を満たすことを保証します。
この説明ではいまいち分かりにくいかもしれません。公式ドキュメントを参考に、もう少し詳しく見ていきます
PartiralOrdは、lt(less than, <)、le(less than or equal to, <=)、gt(greater than, >)、ge(greater than or equal to, >=)の4つを、それぞれ <、<=、>、>=で呼び出します。これらは、互いに、またPartialEqと一貫している必要があります。つまり、以下の条件を満たす必要があります
- a == b if and only if partial_cmp(a, b) == Some(Equal).
- a < b if and only if partial_cmp(a, b) == Some(Less)
- a > b if and only if partial_cmp(a, b) == Some(Greater)
- a <= b if and only if a < b || a == b
- a >= b if and only if a > b || a == b
- a != b if and only if !(a == b).
これを見ると当たり前のことが書かれているような気がします。なぜこんなことを書いているかを理解するためにも、ここで半順序(partial order)について確認しておきましょう。(partial orderについても上の公式ドキュメント上にリンクが貼られています。数学を専攻していた人はもっと詳しく知っていそう。)
二項関係
二項関係という言葉を抑えないと、この後が厳しいため、まず二項関係を説明します。集合
このように書かれると全く分からない人もいるかもしれません。具体例として自然数の集合
さて、二項関係は次のような性質をもつことがあります。
- 反射性: 任意の
について、a (a,a)\in R - 対称性: 任意の
,a について、b ならば、(a,b)\in R (b,a) \in R - 反対称性:任意の
,a について、b かつ(a,b)\in R ならば、(b,a) \in R a=b - 推移性:任意の
についてa,b,c かつ(a,b)\in R ならば、(b,c)\in R (a,c) \in R
例えば自然数の集合
「すうがくすうがくしてて難しい!!」という人に向けて次のような例を考えてみます。あるクラスの生徒たちが互いに友達関係にあると考えます。この場合、二項関係
ここで、
このとき、友情関係
- 反射性: 任意の生徒
について、その人自身との友情関係は存在します。つまりa が(a,a) に含まれます。R - 対称性:生徒
と生徒a が友達関係にあるならば、生徒b と生徒b も友達関係にあると言えます。つまりa が(a,b) に含まれるならば、R も(b,a) に含まれます。R - 推移性:生徒
と生徒a が友達関係にあり。生徒b と生徒b も友達関係にあるならば、生徒c と生徒a も友達関係にあります(多分)。つまりc が(a,b), (b,c) に含まれるならば、R も(a,c) に含まれます。R
半順序
集合
- 反射
- 反対称
- 推移
PartialOrd
整数などの集合に対して、PartialOrd
トレイトは実際にこれを行うトレイトでした。(参照:この記事のPartialOrd
についての最初の6個の式。)
よくよく考えると、PartialOrd
を自分で実装するコードにおいて、返り値がOption<Ordering>だったのはそのような理由からです。
Ord
Ord
はPartialOrd
を実装している型に対してのみ実装できるトレイトでした。Ord
は完全な順序付け(全順序)を行うことができます。完全な順序付けとは、任意の2つの要素に対して、その大小関係が決定できることを意味します。つまり、任意の2つの元Ord
のこの性質から、自分で実装する際には、返り値の型がOrderingであることが分かります。
Ord
を実装する際には、PartialOrd
とEq
が必要であることが分かりました。これは、完全な順序付けを行うためには、大小関係だけでなく等価性も考慮する必要があるためです。Ord
を実装することで、ソートやコレクション型の比較を行うことができます。
また、Rustのf64はソートをする際に一工夫が必要です。f64はNaNを持つことができるため、大小関係を比較する際にはNaNを考慮する必要があります。Nanについては大小比較できないためOrd
が実装されていません。そのため、単純なソートをすることができませんでした。
まとめ
-
PartialOrd
とOrd
は、Rustにおいて値の大小関係を扱うためのトレイトです。 -
PartialOrd
は、半順序を考えます。 -
Ord
は、PartialOrd
が実装されている型に対してのみ実装できます。Ordは全順序であり、任意の2つの値について大小関係が決定できます。 -
Ord
を実装することで、ソートやコレクション型の比較を行うことができます。浮動小数点数型のf64は、NaNの存在によりOrdが実装されていません。そのため、単純なソートを行う際には別の処理が必要になります。
このように、PartialOrdとOrdを適切に使い分けることで、Rustにおいて値の大小関係を扱うことができます。これらのトレイトの理解は、コレクション型の操作やソートなどで重要になります。
Discussion