35

この記事は最終更新日から1年以上が経過しています。

投稿日

更新日

【注意!】トレイトの実装は破壊的変更である

問題

突然ですが問題です。あなたはRustのライブラリ開発者で、以下のようなAPIを公開しているとします。

// 有限な実数のみを扱う型
#[derive(Debug, PartialEq)]
pub struct FiniteFloat(f64);

さてここで、ユーザから「FiniteFloat型とf64型を直接等号比較できるようにしてほしい」というリクエストがあったとします。

そこであなたは、このFiniteFloat型にPartialEqトレイトを実装することにしました。

impl PartialEq<f64> for FiniteFloat {
    fn eq(&self, rhs: &f64) -> bool {
        self.0.eq(rhs)
    }
}

impl PartialEq<FiniteFloat> for f64 {
    fn eq(&self, rhs: &FiniteFloat) -> bool {
        self.eq(&rhs.0)
    }
}

さて、この変更は破壊的でしょうか?

破壊的な作用を及ぼす例

タイトルで既にネタバレしているように、これは破壊的変更です。例えば次のようなコードを考えます。

use finite_float::FiniteFloat;

/// 文字列を","で分割する関数
fn split_into_vec<T>(source: &str) -> Vec<T>
where
    T: FromStr,
    T::Err: std::fmt::Debug,
{
    source
        .split(",")
        .map(|part| part.parse().unwrap())
        .collect::<Vec<T>>()
}

fn main() {
    let result = split_into_vec("1.0,2.0");
    let expected = vec![1.0, 2.0];
    assert_eq!(expected, result);
}

これは先程の変更以前には問題なくコンパイルできていたコードです。では、先程のパッチを当ててコンパイルしてみます。

$ cargo check
    Checking finite_float_test v0.1.0 (...)
error[E0284]: type annotations needed for `Vec<T>`
  --> src/main.rs:32:18
   |
32 |     let result = split_into_vec("1.0,2.0");
   |         ------   ^^^^^^^^^^^^^^ cannot infer type for type parameter `T` declared on the function `split_into_vec`
   |         |
   |         consider giving `result` the explicit type `Vec<T>`, where the type parameter `T` is specified
   |
   = note: cannot satisfy `<_ as FromStr>::Err == _`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0284`.
error: could not compile `finite_float_test`

To learn more, run the command again with --verbose.

コンパイルエラーを吐いてしまいました。

なぜコンパイルエラーが生じたのか

変更を加える前は、f64は同じf64に対してのみPartialEqを実装していました。そのため、Vec<f64>に対して等号比較できるのはVec<f64>だけであるため、コンパイラがresultの型をVec<f64>を推論することが出来ました。

しかし先程の変更により、f64PartialEq<FiniteFloat>を実装するようになったため、f64と等号比較可能な型はf64またはFiniteFloatのどちらかということになります。つまりコンパイラはresultの型がVec<f64>であるのかVec<FiniteFloat>であるのかをassert_eq!の式から推論できなくなり、結果としてコンパイルエラーを生じてしまいました。

破壊的影響を及ぼす他の例

先程の例を見ると、問題を起こしているのはimpl PartialEq<FiniteFloat> for f64の部分であるから、impl PartialEq<f64> for FiniteFloatのように自身の型に対してトレイトを実装するのは問題ないのではないか?と思う人がいるかもしれません。しかし実際にはこれも破壊的な変更になります。

例えばFiniteFloatFromStrトレイトを既に実装しているとします。

impl FromStr for FiniteFloat {
    type Err = <f64 as FromStr>::Err;

    fn from_str(source: &str) -> Result<Self, Self::Err> {
        ...
    }
}

この時、先程のパッチを当ててみると次のようなコードのコンパイルが通らなくなります。

fn main() {
    let result = split_into_vec("1.0,2.0");
    let expected = vec![FiniteFloat(1.0), FiniteFloat(2.0)];
    assert_eq!(expected, result);
}

コンパイルが通らなくなる理由は先程述べたとおりです。

まとめ

公開型に対して公開トレイトを実装した場合、コンパイラがトレイト境界から型を推論できなくなる場合があります。したがって、トレイトの実装は一部の例外を除いて破壊的な変更となるので、バージョン管理には充分気をつけましょう。

コード全文

例1

use std::str::FromStr;

#[derive(PartialEq)]
pub struct FiniteFloat(pub f64);

// 下の実装は破壊的変更!!!
// impl PartialEq<f64> for FiniteFloat {
//     fn eq(&self, rhs: &f64) -> bool {
//         self.0.eq(rhs)
//     }
// }
// 
// impl PartialEq<FiniteFloat> for f64 {
//     fn eq(&self, rhs: &FiniteFloat) -> bool {
//         self.eq(&rhs.0)
//     }
// }

/// 文字列を","で分割する関数
fn split_into_vec<T>(source: &str) -> Vec<T>
where
    T: FromStr,
    T::Err: std::fmt::Debug,
{
    source
        .split(",")
        .map(|part| part.parse().unwrap())
        .collect::<Vec<T>>()
}

fn main() {
    let result = split_into_vec("1.0,2.0");
    let expected = vec![1.0, 2.0];
    assert_eq!(expected, result);
}

例2

use std::str::FromStr;

#[derive(Debug, PartialEq)]
pub struct FiniteFloat(pub f64);

impl FromStr for FiniteFloat {
    type Err = <f64 as FromStr>::Err;

    fn from_str(source: &str) -> Result<Self, Self::Err> {
        f64::from_str(source).map(|f| FiniteFloat(f))
    }
}

// 下の実装は破壊的変更!!!
// impl PartialEq<f64> for FiniteFloat {
//     fn eq(&self, rhs: &f64) -> bool {
//         self.0.eq(rhs)
//     }
// }
// 
// impl PartialEq<FiniteFloat> for f64 {
//     fn eq(&self, rhs: &FiniteFloat) -> bool {
//         self.eq(&rhs.0)
//     }
// }

/// 文字列を","で分割する関数
fn split_into_vec<T>(source: &str) -> Vec<T>
where
    T: FromStr,
    T::Err: std::fmt::Debug,
{
    source
        .split(",")
        .map(|part| part.parse().unwrap())
        .collect::<Vec<T>>()
}

fn main() {
    let result = split_into_vec("1.0,2.0");
    let expected = vec![FiniteFloat(1.0), FiniteFloat(2.0)];
    assert_eq!(expected, result);
}

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について
35