カスタムトレイトを使用して複雑な型境界を避ける
説明
トレイト境界はやや扱いにくくなることがあります。特に、Fn トレイト1のいずれかが関係し、出力型に特定の要件がある場合はそうです。このような場合、新しいトレイトを導入すると、冗長さを減らし、一部の型パラメータを排除し、それによって表現力を高められることがあります。そのようなトレイトには、元の境界を満たすすべての型に対するジェネリックな impl を伴わせることができます。
例
何らかの監視システムや情報収集システムを想像してみましょう。このシステムは、さまざまなソースからさまざまな型の値を取得します。それらの値から、問題を示す何らかのステータスを導出することがあります。たとえば、空きメモリの総量は一定のしきい値を超えているべきであり、id が 0 のユーザーは常に “root” という名前であるべきです。
管理上の理由から、トップレベルではおそらく型消去が必要になるでしょう。しかし、特定の種類のデータソースに対して、具体的な(ユーザーが設定可能な)評価も提供する必要があります(たとえば、数値型に対するしきい値や範囲など)。また、これらの値のソースは多様であるため、呼び出されたときに値を返すクロージャとしてデータソースを提供することを選ぶかもしれません。これらの値はおそらくオペレーティングシステムから取得するため、失敗する可能性のある操作に直面することになりそうです。
したがって、特定の値を扱うために、次のような型とトレイトに落ち着いたとします。
#![allow(unused)]
fn main() {
use std::fmt::Display;
struct Value<G: FnMut() -> Result<T, Error>, S: Fn(&T) -> Status, T: Display> {
value: Option<T>,
getter: G,
status: S,
}
impl<G: FnMut() -> Result<T, Error>, S: Fn(&T) -> Status, T: Display> Value<G, S, T> {
pub fn update(&mut self) -> Result<(), Error> {
(self.getter)().map(|v| self.value = Some(v))
}
pub fn value(&self) -> Option<&T> {
self.value.as_ref()
}
pub fn status(&self) -> Option<Status> {
self.value().map(&self.status)
}
}
// ...
enum Status {
// ...
}
struct Error {
// ...
}
}
これらの型では、少なくともいくつかの箇所で G のトレイト境界を繰り返す必要があります。可読性は低下しますが、その一因はゲッターが Result を返すという事実にあります。“getters” に対する境界を導入すると、より表現力のある境界が可能になり、型パラメータの 1 つを排除できます。
#![allow(unused)]
fn main() {
use std::fmt::Display;
trait Getter {
type Output: Display;
fn get_value(&mut self) -> Result<Self::Output, Error>;
}
impl<F: FnMut() -> Result<T, Error>, T: Display> Getter for F {
type Output = T;
fn get_value(&mut self) -> Result<Self::Output, Error> {
self()
}
}
struct Value<G: Getter, S: Fn(&G::Output) -> Status> {
value: Option<G::Output>,
getter: G,
status: S,
}
// ...
enum Status {}
struct Error;
}
利点
新しいトレイトを導入すると、特に型パラメータの排除を通じて、型境界を単純化するのに役立ちます。新しいトレイトに適切な名前を付けることで、境界の表現力も高まります。この新しいトレイトは抽象化でもあり、それ自体にも次のような機会をもたらします。
- 新しいトレイトを実装する追加の特殊化された型(たとえば、何らかの識別子を表すもの)や、
Defaultなどの他の有用なトレイト - 関連するすべての型に対して実装できる限り、追加のメソッド
欠点
トレイトのような新しい項目を導入するということは、それに適した名前と配置場所を見つける必要があるということです。また、元の機能のユーザーが調べる必要のある項目が 1 つ増えることも意味します2。提示の仕方によっては、上記の例で単純なクロージャを Getter として使用できることがすぐには明らかでない場合があります。