57
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-06

問題

突然ですが問題です。あなたは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);
}
57
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?