PartialEq, PartialOrd

経験的に、これが一番面倒である。 面倒なので最後の方のセクションに持ってきた (最後に書くと楽とは言ってない)。 というか正直勘弁してほしいので、完全なコード例は諦めて概要とコード片だけで説明する。

まず、比較を独自に実装するとしても複数の方法がある。

  • 内部の型と全く同じ比較を用いる
    • MyStr, AsciiStr, AsciiBytes は自然に作ればいずれもこの種類である。
  • 独自スライス型同士では独自の比較を使い、独自スライスと内部の型では内部の型の比較を流用する
    • たとえば「URI 文字列型同士では正規化を行った結果で比較するが、 URI 文字列型と通常の文字列型では単純な文字列比較が行われる」など
      • Uri::new("http://example.com") == Uri::new("http://example.com:80/") かつ Uri::new("http://example.com") != "http://example.com:80/" ということ
    • たとえば「独自スライス型の文字列は特定の文法に従っているので、一部分のみの比較で済む」などの場合
  • すべての型に対して独自の比較を使う
    • たとえば rocket v0.4.6 クレートの UncasedStr など
      • これは比較の際に大文字・小文字の違いを無視する文字列型である。
  • その他
    • たとえば「同じ独自スライス型同士では通常と逆順 (つまり辞書式順序での降順) で比較され、その他の型との比較は許さない」など

どのような比較を定義するかは本当に用途と設計次第なので、私からアドバイスできることは何もない。 強いて言うなら、意味的にマトモな比較を実装しましょうとか、対称性・反対称性・推移性などの要求されている性質を満たすような実装にしましょう1とか、そんなところか。

PartialEqPartialOrd の同時実装

特に独自スライス型においては、配列や文字列に似た性質を持っている場合が多く、 PartialEqPartialOrd の両方を実装したくなる場合が多い。 このような実装はマクロである程度自動化できる。


#![allow(unused)]
fn main() {
// `PartialEq` と `PartialOrd` を同時に実装する例。
// なお、この例では比較アルゴリズムは `str` の比較に丸投げしている。

/// Implement `PartialEq` and `Eq` for the given types.
macro_rules! impl_cmp {
    ($ty_lhs:ty, $ty_rhs:ty) => {
        impl PartialEq<$ty_rhs> for $ty_lhs {
            #[inline]
            fn eq(&self, o: &$ty_rhs) -> bool {
                <str as PartialEq<str>>::eq(AsRef::as_ref(self), AsRef::as_ref(o))
            }
        }
        impl PartialOrd<$ty_rhs> for $ty_lhs {
            #[inline]
            fn partial_cmp(&self, o: &$ty_rhs) -> Option<core::cmp::Ordering> {
                <str as PartialOrd<str>>::partial_cmp(AsRef::as_ref(self), AsRef::as_ref(o))
            }
        }
    };
}
}

左右オペランドの交換

多くの場合、比較の左右オペランドを入れ替えても比較可能にしたいと思うことだろう。 たとえば MyStr == str が可能で str == MyStr が不可能というのはあまり素敵ではないし、実際遭遇すると割とフラストレーションが溜まる2

こういった実装も、さっさとマクロにするに限る。


#![allow(unused)]
fn main() {
// オペランド入れ替えの実装を楽にする例。 先述の `impl_cmp!` マクロを用いた。

macro_rules! impl_cmp {
    ($ty_lhs:ty, $ty_rhs:ty) => {};
}

/// Implement `PartialEq` and `Eq` symmetrically for the given types.
macro_rules! impl_cmp_symmetric {
    ($ty_lhs:ty, $ty_rhs:ty) => {
        impl_cmp!($ty_lhs, $ty_rhs);
        impl_cmp!($ty_rhs, $ty_lhs);
    };
}
}

いくら書いても満たされることのない比較実装欲 (?)

たとえばこれらのマクロを使って MyStr に比較を実装しようとすると、こうなる。


#![allow(unused)]
fn main() {
// オペランド入れ替えの実装を楽にする例。 先述の `impl_cmp_symmetric!` マクロを用いた。
// 残念ながら完全ではない。

use std::borrow::Cow;

#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
struct MyStr(str);

impl AsRef<str> for MyStr {
    fn as_ref(&self) -> &str { unimplemented!() }
}

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct MyString(String);

impl ToOwned for MyStr {
    type Owned = MyString;

    fn to_owned(&self) -> Self::Owned { unimplemented!() }
}

impl std::borrow::Borrow<MyStr> for MyString {
    fn borrow(&self) -> &MyStr { unimplemented!() }
}

impl AsRef<str> for MyString {
    fn as_ref(&self) -> &str { unimplemented!() }
}

impl AsRef<str> for Box<MyStr> {
    fn as_ref(&self) -> &str { unimplemented!() }
}

macro_rules! impl_cmp {
    ($ty_lhs:ty, $ty_rhs:ty) => {
        impl PartialEq<$ty_rhs> for $ty_lhs {
            #[inline]
            fn eq(&self, o: &$ty_rhs) -> bool {
                <str as PartialEq<str>>::eq(AsRef::as_ref(self), AsRef::as_ref(o))
            }
        }
        impl PartialOrd<$ty_rhs> for $ty_lhs {
            #[inline]
            fn partial_cmp(&self, o: &$ty_rhs) -> Option<core::cmp::Ordering> {
                <str as PartialOrd<str>>::partial_cmp(AsRef::as_ref(self), AsRef::as_ref(o))
            }
        }
    };
}

macro_rules! impl_cmp_symmetric {
    ($ty_lhs:ty, $ty_rhs:ty) => {
        impl_cmp!($ty_lhs, $ty_rhs);
        impl_cmp!($ty_rhs, $ty_lhs);
    };
}

impl_cmp_symmetric!(MyStr, str);
impl_cmp_symmetric!(MyStr, &str);
impl_cmp_symmetric!(&MyStr, str);

impl_cmp_symmetric!(MyStr, String);
impl_cmp_symmetric!(MyStr, &String);
impl_cmp_symmetric!(&MyStr, String);
impl_cmp_symmetric!(MyStr, Box<str>);
impl_cmp_symmetric!(&MyStr, Box<str>);
impl_cmp_symmetric!(Box<MyStr>, str);
impl_cmp_symmetric!(Box<MyStr>, &str);
impl_cmp_symmetric!(MyStr, Cow<'_, str>);
impl_cmp_symmetric!(&MyStr, Cow<'_, str>);

impl_cmp_symmetric!(MyString, &MyString);
impl_cmp_symmetric!(MyString, MyStr);
impl_cmp_symmetric!(MyString, &MyStr);
impl_cmp_symmetric!(MyString, String);
impl_cmp_symmetric!(MyString, &String);
impl_cmp_symmetric!(&MyString, String);
impl_cmp_symmetric!(MyString, str);
impl_cmp_symmetric!(MyString, &str);
impl_cmp_symmetric!(&MyString, str);
}

不思議なことに、この比較というのがいくら実装しても後から足りないものが出てくるのである3。 特にありがちなのは、内側の型関係、所有権の有無関係、互換性のある別の型、 BoxRcArcCow、参照の有無、参照の mutability などなど。 本当にやっていられないので、気付いてから足すくらいの気持ちで良い。

比較方式の使い分け

比較対象の型次第で、内部的にどの比較実装を使うかを分けたい場合がある。 たとえば AsciiBytesstr の比較なら <str as PartialEq<str>>::eq で良いかもしれないが、 AsciiBytes[u8] の比較では str の比較に丸投げすることはできないため、 <[u8] as PairtialEq<[u8]>>::eq を使うことになろう。 あるいは「大文字・小文字を区別しない文字列型」のようなカスタム比較を入れたい場合、 <CustomStr as PartialEq<CustomStr>>::eq<str as PartialEq<str>>::eq を使い分けたくなることもあるかもしれない。

そのような場合、上で紹介したマクロに一工夫入れて、「どの型の Partial{Eq,Ord} を使って実装するか」もマクロ引数にするとよい。


#![allow(unused)]
fn main() {
// `$ty_common` で、両辺の型から `AsRef` で変換できる、比較に使う共通の型を受け取る。

/// Implement `PartialEq` and `Eq` for the given types.
macro_rules! impl_cmp {
    ($ty_common:ty, $ty_lhs:ty, $ty_rhs:ty) => {
        impl PartialEq<$ty_rhs> for $ty_lhs {
            #[inline]
            fn eq(&self, o: &$ty_rhs) -> bool {
                <$ty_common as PartialEq<$ty_common>>::eq(AsRef::as_ref(self), AsRef::as_ref(o))
            }
        }
        impl PartialOrd<$ty_rhs> for $ty_lhs {
            #[inline]
            fn partial_cmp(&self, o: &$ty_rhs) -> Option<core::cmp::Ordering> {
                <$ty_common as PartialOrd<$ty_common>>::partial_cmp(
                    AsRef::as_ref(self),
                    AsRef::as_ref(o),
                )
            }
        }
    };
}

/// Implement `PartialEq` and `Eq` symmetrically for the given types.
macro_rules! impl_cmp_symmetric {
    ($ty_common:ty, $ty_lhs:ty, $ty_rhs:ty) => {
        impl_cmp!($ty_common, $ty_lhs, $ty_rhs);
        impl_cmp!($ty_common, $ty_rhs, $ty_lhs);
    };
}
}

以下は、これらのマクロを使って AsciiBytes に対して str 類と [u8] 類両方との比較を実装する例である。


#![allow(unused)]
fn main() {
// `AsciiBytes` 系の型に対する比較の実装例。

use std::borrow::Cow;

#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
struct AsciiBytes(str);

impl AsRef<str> for AsciiBytes {
    fn as_ref(&self) -> &str { unimplemented!() }
}

impl AsRef<[u8]> for AsciiBytes {
    fn as_ref(&self) -> &[u8] { unimplemented!() }
}

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct AsciiByteBuf(String);

impl ToOwned for AsciiBytes {
    type Owned = AsciiByteBuf;

    fn to_owned(&self) -> Self::Owned { unimplemented!() }
}

impl std::borrow::Borrow<AsciiBytes> for AsciiByteBuf {
    fn borrow(&self) -> &AsciiBytes { unimplemented!() }
}

impl AsRef<str> for AsciiByteBuf {
    fn as_ref(&self) -> &str { unimplemented!() }
}

impl AsRef<[u8]> for AsciiByteBuf {
    fn as_ref(&self) -> &[u8] { unimplemented!() }
}

impl AsRef<str> for Box<AsciiBytes> {
    fn as_ref(&self) -> &str { unimplemented!() }
}

impl AsRef<[u8]> for Box<AsciiBytes> {
    fn as_ref(&self) -> &[u8] { unimplemented!() }
}

macro_rules! impl_cmp {
    ($ty_common:ty, $ty_lhs:ty, $ty_rhs:ty) => {
        impl PartialEq<$ty_rhs> for $ty_lhs {
            #[inline]
            fn eq(&self, o: &$ty_rhs) -> bool {
                <$ty_common as PartialEq<$ty_common>>::eq(AsRef::as_ref(self), AsRef::as_ref(o))
            }
        }
        impl PartialOrd<$ty_rhs> for $ty_lhs {
            #[inline]
            fn partial_cmp(&self, o: &$ty_rhs) -> Option<core::cmp::Ordering> {
                <$ty_common as PartialOrd<$ty_common>>::partial_cmp(
                    AsRef::as_ref(self),
                    AsRef::as_ref(o),
                )
            }
        }
    };
}

/// Implement `PartialEq` and `Eq` symmetrically for the given types.
macro_rules! impl_cmp_symmetric {
    ($ty_common:ty, $ty_lhs:ty, $ty_rhs:ty) => {
        impl_cmp!($ty_common, $ty_lhs, $ty_rhs);
        impl_cmp!($ty_common, $ty_rhs, $ty_lhs);
    };
}

impl_cmp_symmetric!(str, AsciiBytes, str);
impl_cmp_symmetric!(str, AsciiBytes, &str);
impl_cmp_symmetric!(str, &AsciiBytes, str);
impl_cmp_symmetric!([u8], AsciiBytes, [u8]);
impl_cmp_symmetric!([u8], AsciiBytes, &[u8]);
impl_cmp_symmetric!([u8], &AsciiBytes, [u8]);

impl_cmp_symmetric!(str, AsciiBytes, String);
impl_cmp_symmetric!(str, AsciiBytes, &String);
impl_cmp_symmetric!(str, AsciiBytes, Box<str>);
impl_cmp_symmetric!(str, &AsciiBytes, Box<str>);
impl_cmp_symmetric!(str, Box<AsciiBytes>, str);
impl_cmp_symmetric!(str, Box<AsciiBytes>, &str);
impl_cmp_symmetric!(str, AsciiBytes, Cow<'_, str>);
impl_cmp_symmetric!(str, &AsciiBytes, Cow<'_, str>);
impl_cmp_symmetric!([u8], AsciiBytes, Vec<u8>);
impl_cmp_symmetric!([u8], AsciiBytes, &Vec<u8>);
impl_cmp_symmetric!([u8], AsciiBytes, Box<[u8]>);
impl_cmp_symmetric!([u8], &AsciiBytes, Box<[u8]>);
impl_cmp_symmetric!([u8], Box<AsciiBytes>, [u8]);
impl_cmp_symmetric!([u8], Box<AsciiBytes>, &[u8]);
impl_cmp_symmetric!([u8], AsciiBytes, Cow<'_, [u8]>);
impl_cmp_symmetric!([u8], &AsciiBytes, Cow<'_, [u8]>);

impl_cmp_symmetric!(str, AsciiByteBuf, &AsciiByteBuf);
impl_cmp_symmetric!(str, AsciiByteBuf, AsciiBytes);
impl_cmp_symmetric!(str, AsciiByteBuf, &AsciiBytes);
impl_cmp_symmetric!(str, &AsciiByteBuf, AsciiBytes);
impl_cmp_symmetric!(str, AsciiByteBuf, str);
impl_cmp_symmetric!(str, AsciiByteBuf, &str);
impl_cmp_symmetric!(str, &AsciiByteBuf, str);
impl_cmp_symmetric!([u8], AsciiByteBuf, [u8]);
impl_cmp_symmetric!([u8], AsciiByteBuf, &[u8]);
impl_cmp_symmetric!([u8], &AsciiByteBuf, [u8]);
impl_cmp_symmetric!(str, AsciiByteBuf, String);
impl_cmp_symmetric!(str, AsciiByteBuf, &String);
impl_cmp_symmetric!(str, &AsciiByteBuf, String);
impl_cmp_symmetric!([u8], AsciiByteBuf, Vec<u8>);
impl_cmp_symmetric!([u8], AsciiByteBuf, &Vec<u8>);
impl_cmp_symmetric!([u8], &AsciiByteBuf, Vec<u8>);
}

1

利便性を追求したつもりで PartialEq が満たすべき性質を破ってしまう実装の例などが PartialEq のドキュメントで紹介されており、参考になる。 是非一読されることをおすすめする。

2

具体例としては proc-macro2 v1.0.24 クレートの Ident 型などがある。 これは Ident == str の比較ができるのに str == Ident の比較ができないという代物で、私も何度か悲しいコンパイルエラーを出したことがある。

3

Rust の標準ライブラリでも実際そんな感じで、 str 系や Path 系などでたまに比較が追加されたりなどしているっぽい