問題
突然ですが問題です。あなたは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>
を推論することが出来ました。
しかし先程の変更により、f64
はPartialEq<FiniteFloat>
を実装するようになったため、f64
と等号比較可能な型はf64
またはFiniteFloat
のどちらかということになります。つまりコンパイラはresult
の型がVec<f64>
であるのかVec<FiniteFloat>
であるのかをassert_eq!
の式から推論できなくなり、結果としてコンパイルエラーを生じてしまいました。
破壊的影響を及ぼす他の例
先程の例を見ると、問題を起こしているのはimpl PartialEq<FiniteFloat> for f64
の部分であるから、impl PartialEq<f64> for FiniteFloat
のように自身の型に対してトレイトを実装するのは問題ないのではないか?と思う人がいるかもしれません。しかし実際にはこれも破壊的な変更になります。
例えばFiniteFloat
がFromStr
トレイトを既に実装しているとします。
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);
}