Progrust Library.

// dictionary

比較のトレイト.

値の等しさと大小を比べる振る舞いは、標準ライブラリのstd::cmpにある4つのトレイトが担っています。等価性をPartialEqとEqが、順序づけをPartialOrdとOrdが担当し、Partialの付かない側は付く側より強い数学的性質を型に要求します。

比較演算子(==や<)はこれらのメソッド呼び出しに展開されるため、自作の型を比較できるようにするには、この4つのうち必要なものを実装します。多くの場合はderive属性で導出するだけで済みます。

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Price(u32); // 税込価格(円)

fn main() {
    let mut prices = vec![Price(320), Price(120), Price(980)];
    prices.sort(); // sortはOrdを要求する
    println!("{:?}", prices);
    println!("最安: {:?}", prices.iter().min().unwrap()); // minもOrd
    println!("同額か: {}", Price(120) == Price(120)); // ==はPartialEq
}
Playgroundで開く

4つのトレイトの関係

EqはPartialEqを、OrdはEqとPartialOrdを、それぞれスーパートレイトとして継承しています。つまりOrdを実装する型は、残り3つもすべて実装している必要があります。各トレイトが要求するメソッドと、追加で保証する性質は次のとおりです。

トレイト スーパートレイト 要求メソッド 対応する演算子 追加で保証する性質
PartialEq<Rhs = Self> なし eq ==・!= 対称律・推移律1
Eq PartialEq なし — 反射律(a == a)2
PartialOrd<Rhs = Self> PartialEq partial_cmp <・<=・>・>= 推移律・双対性3
Ord EqとPartialOrd cmp — 全順序(a < b・a == b・a > bのちょうど1つが成立)4

EqとOrdは演算子を新たに増やすわけではなく、Eqに至ってはメソッドを1つも持ちません。

「部分」と「全」の違い

Partialは「比較が成立しない組み合わせがあってもよい」という意味です。この違いは、要求メソッドの戻り値の型にそのまま表れています。

  • PartialOrdのpartial_cmpはOption型(Option<Ordering>)を返し、順序が決まらない場合にNoneを返せます
  • OrdのcmpはOrdering(Less・Equal・Greaterのいずれか)を必ず返すため、どの2つの値を比べても必ず決着が付きます

等価性の側も同様で、PartialEqが要求するのは対称律と推移律だけです。a == aが成り立つこと(反射律)までは求めていないため、自分自身と等しくならない値が混じっていても構いません。Eqはそこに反射律を足して、比較を数学でいう同値関係にします2。

fn main() {
    println!("{:?}", 3.partial_cmp(&5)); // Some(Less)
    println!("{:?}", 3.cmp(&5));         // Less(Ordなので必ず決まる)
    println!("{:?}", f64::NAN.partial_cmp(&1.0)); // None(順序が決まらない)
}
Playgroundで開く

浮動小数点型がEqを実装しない理由

浮動小数点型(f32・f64)はPartialEqとPartialOrdだけを実装し、EqとOrdは実装しません。IEEE 754が定めるNaN(非数)が原因です。

NaNは自分自身とも等しくならず、NaN == NaNはfalseになります。これは反射律a == aに反するため、Eqの要求を満たせません12。順序についてもNaNはどの値とも大小が決まらず、partial_cmpがNoneを返すため、必ず決着が付くことを求めるOrdも実装できません3。

実害が出るのはソートなどの場面です。slice::sortはT: Ordというトレイト境界を持つため、要素がf64のベクタ(Vec)はそのままでは並べ替えられません。比較の仕方を自分で渡すsort_byを使い、IEEE 754のtotalOrderに従って全順序を与えるtotal_cmp(Rust 1.62以降)を呼びます5。

fn main() {
    let mut weights = vec![1.5_f64, 0.3, 2.25]; // 商品の重さ(kg)
    // f64はOrdを実装しないため、sortはエラー: E0277になる
    weights.sort(); 
    weights.sort_by(|a, b| a.total_cmp(b)); 
    println!("{:?}", weights); // [0.3, 1.5, 2.25]
}
Playgroundで開く

補足

deriveするときの組み合わせ

継承関係があるため、derive属性でも下位のトレイトを一緒に導出する必要があります。#[derive(Eq)]だけを書くと、スーパートレイトのPartialEqが実装されていないためコンパイルエラーになります。

#[derive(Eq)] // エラー: E0277(PartialEqを実装していない)
struct Price(u32);

fn main() {
    println!("{}", Price(120).0);
}
Playgroundで開く

実際には次の4通りのどれかを書くことになります。

書き方 できるようになること
#[derive(PartialEq)] ==・!=での比較
#[derive(PartialEq, Eq)] 上に加えてEqを要求するAPIで使える
#[derive(PartialEq, PartialOrd)] 上に加えて<などでの比較(f64のフィールドを持つ型はここまで)
#[derive(PartialEq, Eq, PartialOrd, Ord)] 上に加えてOrdを要求するAPIで使える

導出される比較は構造体ならフィールドの宣言順、列挙型ならバリアントの判別子(既定では宣言順)を優先度とする辞書式比較です3。フィールドやバリアントの並び替えが比較結果を変えてしまう点に注意してください。

手で実装するときはOrdに寄せる

!=を担うneと、<などを担うlt・le・gt・geにはデフォルト実装があるため、手で書くのはeq・partial_cmp・cmpの3つだけです13。

std公式ドキュメントは、Ordまで実装する型では4つともderive属性で導出するか、4つともcmpの実装をもとに手で書くかのどちらかに揃えることを勧めています4。一部だけを導出して残りを手書きすると、==とcmpの答えが食い違いやすいためです1。手で書く場合は比較のロジックをcmpに集約し、残りはそこへ委譲します。

use std::cmp::Ordering;

struct Book {
    title: String,
    price: u32,
}

impl Ord for Book {
    fn cmp(&self, other: &Self) -> Ordering {
        // 価格順。同額なら題名順で決着させる
        self.price.cmp(&other.price).then(self.title.cmp(&other.title))
    }
}

impl PartialOrd for Book {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other)) // Ordに委譲する
    }
}

impl PartialEq for Book {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal // 同じくOrdに委譲する
    }
}

impl Eq for Book {}

fn main() {
    let cheap = Book { title: String::from("Rust入門"), price: 3200 };
    let pricey = Book { title: String::from("Rust実践"), price: 4200 };
    println!("{}", cheap < pricey);  // true
    println!("{}", cheap == pricey); // false
}
Playgroundで開く
EqやOrdを要求する標準ライブラリのAPI

EqとOrdはメソッドを持たない(あるいは増やさない)にもかかわらず、標準ライブラリの各所でトレイト境界として要求されます。

API 要求される境界 理由
slice::sort・Iterator::max・Iterator::min Ord どの2値も必ず順序が決まらないと結果が定まらないため
BTreeMap・BTreeSetのキー Ord キーを順序どおりに並べて保持するため
HashMap・HashSetのキー Eq + Hash 等しいキーのハッシュ値は必ず等しい、という前提で探索するため6

比較の仕方を引数で受け取るsort_by・max_byなどはOrdを要求しないため、f64のようにOrdを実装しない型でも使えます。

演算子との対応

==・!=がPartialEqに、<・<=・>・>=がPartialOrdに対応するように、Rustの演算子の多くは対応するトレイトの実装として定義されています。+ならstd::ops::Addといった具合です。この仕組みを演算子オーバーロードと呼びます。

なおPartialEq<Rhs = Self>・PartialOrd<Rhs = Self>は右辺の型を型引数に取り、その既定値がSelfであるために「同じ型同士」が原則になります。異なる型と比べたいときは、impl PartialEq<別の型> for 自分の型のようにRhsを明示して実装します1。Stringと&strが==で比べられるのも、標準ライブラリがこの形の実装を用意しているためです。

Footnotes

  1. std公式ドキュメント — Trait PartialEq “Trait for comparisons using the equality operator.” / 定義はpub trait PartialEq<Rhs = Self> where Rhs: ?Sized、必須メソッドはeq、neは提供メソッド(“The default implementation is almost always sufficient”)。要求される性質: “symmetric: if A: PartialEq<B> and B: PartialEq<A>, then a == b implies b == a” / “transitive: … a == b and b == c implies a == c” / “This trait allows for comparisons using the equality operator, for types that do not have a full equivalence relation. For example, in floating point numbers NaN != NaN, so floating point types implement PartialEq but not Eq.” / “If PartialOrd or Ord are also implemented for Self and Rhs, their methods must also be consistent with PartialEq … It’s easy to accidentally make them disagree by deriving some of the traits and manually implementing others.” / 異なる型との比較: “How can I compare two different types? The type you can compare with is controlled by PartialEq’s type parameter.”(impl PartialEq<BookFormat> for Bookの例が示されている) ↩ ↩2 ↩3 ↩4 ↩5

  2. std公式ドキュメント — Trait Eq 定義はpub trait Eq: PartialEq。“The primary difference to PartialEq is the additional requirement for reflexivity.” / “Eq, which builds on top of PartialEq also implies: reflexive: a == a” / “This property cannot be checked by the compiler, and therefore Eq is a trait without methods.” / “Violating this property is a logic error. The behavior resulting from a logic error is not specified, but users of the trait must ensure that such logic errors do not result in undefined behavior.” / “Floating point types such as f32 and f64 implement only PartialEq but not Eq because NaN != NaN.”(Rust 1.93のstdソースcore/src/cmp.rsで確認) ↩ ↩2 ↩3 ↩4 ↩5

  3. std公式ドキュメント — Trait PartialOrd 定義はpub trait PartialOrd<Rhs = Self>: PartialEq<Rhs>、必須メソッドはpartial_cmp、lt・le・gt・geは提供メソッド。要求される性質は “Transitivity” と “Duality” の2つで、“irreflexivity of < and >: !(a < a), !(a > a)” は、そこから導かれる系(Corollaries)として記載されている。“partial_cmp returns None if the values are not comparable”(f64::NAN.partial_cmp(&1.0) == Noneの例あり)。導出: “When derived on structs, it will produce a lexicographic ordering based on the top-to-bottom declaration order of the struct’s members. When derived on enums, variants are primarily ordered by their discriminants.” 実装の指針: “If your type is Ord, you can implement partial_cmp by using cmp” (Some(self.cmp(other)))。 ↩ ↩2 ↩3 ↩4

  4. std公式ドキュメント — Trait Ord 定義はpub trait Ord: Eq + PartialOrd、必須メソッドはcmp。要件は “Implementations must be consistent with the PartialOrd implementation” / “partial_cmp(a, b) == Some(cmp(a, b))”。本文の「ちょうど1つが成立」は、そこから導かれる系(Corollaries)“From the above and the requirements of PartialOrd, it follows that … exactly one of a < b, a == b or a > b is true” にあたる。厳密には “Mathematically speaking, the < operator defines a strict weak order. In cases where == conforms to mathematical equality, it also defines a strict total order.” とされている。実装方針は “If you derive it, you should derive all four traits. If you implement it manually, you should manually implement all four traits, based on the implementation of Ord.”。“Violating these requirements is a logic error. The behavior resulting from a logic error is not specified, but users of the trait must ensure that such logic errors do not result in undefined behavior.”(Rust 1.93のstdソースcore/src/cmp.rsで確認) ↩ ↩2 ↩3

  5. std公式ドキュメント — f64::total_cmp “Unlike the standard partial comparison between floating point numbers, this comparison always produces an ordering in accordance to the totalOrder predicate as defined in the IEEE 754 (2008 revision) floating point standard.” 安定化バージョンは#[stable(feature = "total_cmp", since = "1.62.0")](Rust 1.93のソースで確認)。slice::sortの境界T: Ordも同ソースで確認。 ↩

  6. std公式ドキュメント — Struct HashMap “It is required that the keys implement the Eq and Hash traits, although this can frequently be achieved by using #[derive(PartialEq, Eq, Hash)]. If you implement these yourself, it is important that the following property holds: k1 == k2 -> hash(k1) == hash(k2). … Violating this property is a logic error.” ↩

この辞書が使われているページ

backlinks 5

  1. 辞書derive属性
  2. 辞書演算子オーバーロード
  3. 辞書比較演算子
  4. 本動かして学ぶRustプログラミング問題集 › 第19章 derive属性と標準トレイト
  5. 本動かして学ぶRustプログラミング問題集 › 第20章 トレイト境界とimpl Trait