- Start Date: 2017-01-19
- RFC: https://rust-lang.github.io/rfcs/1859-try-trait.html
- PR: rust-lang/rfcs#1859
- Issue: rust-lang/rust#31436
?の挙動をカスタマイズするためのTryトレイトを導入するResult以外にも?が適用可能になる
Resultと?演算子の組み合わせは便利だけど、それ以外の型とも組み合わせて使えるべき。
rustfmtのソースコードから抜粋した、以下の二行を考えてみる:
let lhs_budget = try_opt!(width.checked_sub(prefix.len() + infix.len()));
let rhs_budget = try_opt!(width.checked_sub(suffix.len()));これらはOptionに対して、独自定義のtry_opt!を用いて、?と同様の早期脱出を実現している。
このRFCにより、?を用いて次のように書けるようになる。
let lhs_budget = width.checked_sub(prefix.len() + infix.len())?;
let rhs_budget = width.checked_sub(suffix.len())?;try!に対する?の利点が、上の例でも同様に当てはまる:
- 接尾記法は、よりなめらかなAPIを許可する (!
foo()?.bar()?.baz()?的な話?) - 気付きやすくありながら簡潔
ただし?の振る舞いを、ResultやOptionに対してハードコードするのは避け、拡張可能なようにしたい。
例えばFutureの適用結果を表現するためには、以下のような型が考えられる。
enum Poll<T, E> {
Ready(T),
NotReady,
Error(E),
}この場合は「Poll::Ready以外が返ってきたら即座に関数から抜ける」といった挙動を?で実現したい。
既存のtry!マクロと?演算子は、既に制限付きでの型変換を許容している:
- 早期脱出の際に
Result型のErr部に対してFrom::fromを勝手に適用してくれる - エラーの場合によく使われる:
// ! RFC内には存在しない例 fn foo() -> Result<(), OuterError> { // a) `?`を使った場合 bar()?; // b) 上のコードの展開形(イメージ) if let Err(e): Result<_, InnerError> = bar() { // `OuterError`が`From<InnerError>`トレイトを実装していれば、 // `?`内で以下のような型変換が自動で行われる。 return Err(OtherError::from(e)); } Ok(()) }
- 例えば、関数内で発生するいろいろなエラーを
Box<Error>のような、より共通的なものにupcastする時等に使用される
より進んで、異なる型同士での相互変換が行えると嬉しいケースがある:
- e.g.,
Result<T, HttpError>とHttpResponseの相互変換 (rust-lang/rfcs#1718 (comment)) - e.g., 上に出てきた
PollとResultの相互変換// NOTE: `bar()`は、`Poll`を返す関数 // a) 返り値が`Poll`の場合: `Poll` => `Poll` fn foo() -> Poll<T, E> { let x = bar()?; // propagate error case ... } // b) 返り値が`Result`の場合: `Poll` => `Result` fn foo() -> Result<T, E> { let x = bar()?; // propagate error case ... }
注意点:
- 相互変換は意図的に行われるようにするべき
- 例えば、
ResultからOptionへの変換を許容してしまうのはリスクがあるResultは"未処理エラー"を表すのによく使われるのに対して、Optionはそうではない?で暗黙的に変換できてしまうと、エラーを予期せずに見落としてしまう危険性がある
- このようなリスクを緩和するために、偶発的な変換の発生防止用のいくつかの変換を、このRFCでは採用している(詳細は後述)
ここで、RFCで定義されているトレイトとその実装を、実際に試せるよ。
?は、以下のように展開されるように変更される(Tryは後述):
match Try::into_result(expr) {
Ok(v) => v,
// `Result`という共通形式を通して、型変換を行っている。
//
// 仮に`expr`の型を`T`、帰り値の型を`U`とすると:
// 1. exprを`T::Error`に変換 (`Try::into_result`)
// 2. `T::Error`を`U::Error`に変換 (`From::from`)
// 3. `U::Error`を`U`に変換 (`Try::from_error`)
Err(e) => return Try::from_error(From::from(e)),
}Tryはlibcore::opsに追加されるトレイト (std::opsでも参照可能):
trait Try {
type Ok;
type Error;
/// Applies the "?" operator. A return of `Ok(t)` means that the
/// execution should continue normally, and the result of `?` is the
/// value `t`. A return of `Err(e)` means that execution should branch
/// to the innermost enclosing `catch`, or return from the function.
///
/// If an `Err(e)` result is returned, the value `e` will be "wrapped"
/// in the return type of the enclosing scope (which must itself implement
/// `Try`). Specifically, the value `X::from_error(From::from(e))`
/// is returned, where `X` is the return type of the enclosing function.
fn into_result(self) -> Result<Self::Ok, Self::Error>;
/// Wrap an error value to construct the composite result. For example,
/// `Result::Err(x)` and `Result::from_error(x)` are equivalent.
fn from_error(v: Self::Error) -> Self;
/// Wrap an OK value to construct the composite result. For example,
/// `Result::Ok(x)` and `Result::from_ok(x)` are equivalent.
///
/// *The following function has an anticipated use, but is not used
/// in this RFC. It is included because we would not want to stabilize
/// the trait without including it.*
fn from_ok(v: Self::Ok) -> Self;
}libforeは、以下の実装を備えるはず。
Result用(今の同じ動作):
impl<T,E> Try for Result<T, E> {
type Ok = T;
type Error = E;
fn into_result(self) -> Self {
self
}
fn from_ok(v: T) -> Self {
Ok(v)
}
fn from_error(v: E) -> Self {
Err(v)
}
}Option用:
mod option {
pub struct Missing;
impl<T> Try for Option<T> {
type Ok = T;
type Error = Missing;
fn into_result(self) -> Result<T, Missing> {
self.ok_or(Missing)
}
fn from_ok(v: T) -> Self {
Some(v)
}
fn from_error(_: Missing) -> Self {
None
}
}
}Missingの使い方が特徴的:
()ではなく、Optionのための型を新設:ResultからOptionに誤って変換してしまう危険性を軽減するためResult<T, Missing>からしかOption<T>には変換できない- 参考: rust-lang/rfcs#1859 (comment)
Missingのような型(fresh type)の使い方は、#[must_use]属性を持たないような型にTryを実装する場合にはいつでも推奨される- より意味論的に述べるなら「"unhandled error"を表現しない型」に対して実装する場合
このRFCにより?の返り値がResult以外にもなり得るようになるので、型推論は困難になる。
例えば今はvec.iter().map(|e| ..).collect()?と書けるのが、
以下のように型を明示しなければならなくなる(あるいはtry!マクロを使うか):
vec.iter().map(|e| ...).collect::<Result<_, _>>()?別の問題:
f()??みたいにネストできないFrom::from(From::from(err))のようなもの- 変換の最初と最後の型は分かるが、中間の型が不明瞭
- 参考: https://internals.rust-lang.org/t/pre-rfc-fold-ok-is-composable-internal-iteration/4434/23
try!でも同様だし、このRFCとは直行する問題に見えるので、ここでは扱わない:- 解決したいなら推論フォールバック的なものを導入する必要がある?
このRFCは既存の?演算子の拡張なので、以下の段階を踏むのが良い:
-
- 最初は
Resultの例を提示
- 最初は
-
?がオーバーロード可能なことにも言及し、詳細なページへのリンクを貼る
-
- そこでは
Optionに適用可能なことを説明
- そこでは
-
- さらにはデシュガーについても述べて、自前の型に対して実装可能なようにする
Rust bookやRust by examplesの内容も更新する。
option::Missingのような特別なエラー型を導入するのが適切なケースについてのガイドラインも発行すべき。
変換が行えないケースでのエラーメッセージの内容も重要。
今はCarrierへの参照を示すだけで分かりにくい。
以下のようなエラーメッセージを出すことができる:
`?` cannot be applied to a value of type `Foo`
返り値の型を()とすると、以下のようなメッセージが可能:
cannot use the `?` operator in a function that returns `()`
あるいはもっと厳密に、次のようにしたいかもしれない:
`?` cannot be applied to a `Result<T, Box<Error>>` in a function that returns `()`
このケースだと「Result<(), Box<Error>>に返り値を変えてはどうですか?」と示唆したいかもしれない。
ただしトレイトの実装メソッドやmain関数の中の場合は、ユーザが勝手に型を変更できないので、この示唆は行いたくない。
ただ、トレイトが同じcrate内で定義されているならしても良いかも。
返り値の型Rが、Tryは実装しているけど、結果エラーからは構築できない場合
(e.g., R = Option<T>だけど?はResult<T, ()>に適用された):
`?` cannot be applied to a `Result<T, Box<Error>>` in a function that returns `Option<T>`
この変換失敗は、以下の二つの理由から発生し得るので、紛らわしく成り得る:
- a.
From実装がない(おそらくミス) - b.
Tryの実装が意図的に制限されている (e.g.,Option)
以下のようなメッセージを出すことは、ユーザが状況を診断する助けとなり得る:
22 | fn foo(...) -> Option<T> {
| --------- requires an error of type `option::Missing`
| write!(foo, ...)?;
| ^^^^^^^^^^^^^^^^^ produces an error of type `io::Error`
| }catchが安定化されたら、返り値の型との齟齬による変換エラーの場合にはcatchの使用を示唆できるかも
(「catchを使用するか、返り値の型を変更してはどうでしょうか?」)。
返り値の型が変更できないケースでは、以下のように該当部分を別のヘルパ関数に切り出すリファクタリングを示唆しても良いかもしれない。
fn inner_main() -> Result<(), HLError> {
let args = parse_cmdline()?;
// all the real work here
}
fn main() {
process::exit(match inner_main() {
Ok(_) => 0,
Err(ref e) => {
writeln!(io::stderr(), "{}", e).unwrap();
1
}
});
}?のデシュガーが「ASTからHIRの変換時」ではなく「HIRからMIRへの変換時」に行われると、エラーメッセージの改善に有用かもしれない。
ただし、おそらくソースアノテーションを使えば十分。
Result型以外のサポートにより、型推論は難しくなる- "must use"の値が誤って別の型に変換され見過ごされてしまう危険性
option::Missingのような型の導入により緩和はされる
このRFCの最初の提案時にはTryは以下のような形だった(今と全く異なる):
trait Try<E> {
type Success;
// 実装型(e.g., `Option`)を`Result`に変換 (! 一方向変換?)
fn try(self) -> Result<Self::Success, E>;
}エラー型Eをパラメータとして受け取っていることに注目:
- 文脈を考慮した変換が可能となる
- e.g.,
Eの型がFooかBarかで、実装(変換)を変えられる
これは現在の"還元主義者"的なアプローチに変更された:
-
- 最初に文脈非依存の方法でOk/Error値に変換
-
- 文脈に沿う型に
from_errorを使って変換
- 文脈に沿う型に
おおまかな変更理由:
- トレイトがよりシンプルで直観的になる
from_okも簡単にサポート可能
- 文脈依存の挙動は、全く予期せぬ動作に繋がる可能性がある
option::Missingのような特別な型の使用により、もともとのデザインが回避したいと考えていた問題(過度に緩い相互変換の回避)を緩和可能?のデシュガーにFromを使うのは良いこと:- いろいろな型に対して、広く使われサポートされているため
- 孤児ルール(RFC 1023)との親和性が若干良い:
- 例:
- "本質主義者"のアプローチで「
Pollをyieldする関数内でResultを返す(変換する)ことを許可したい」とする impl<T,E> Try<Poll<T,E>> for Result<T,E>のような実装が必要- => 孤児ルールに抵触する(i.e., 他のcrateの型に対する
implは不許可)
- "本質主義者"のアプローチで「
- 例:
ResultとOptionの自由な相互変換を避けたい、ということなら高階型(HKT)が適切に見えるかも- ただ、今はRustでHKTは使えない
- また、この問題に対してHKTが特別適切、という訳ではないことも分かっている
- 以下のように異なるkindが欲しくなる:
type -> type(OptionにTryを実装する場合)type -> type -> type(ResultにTryを実装する場合)type(boolにTryを実装する場合)
- 以下のように異なるkindが欲しくなる:
いろいろな名前が提案された:
Carrier:- もともとの名前
- 実装型が、エラー値の"運び屋"となるため
QuestionMark:?演算子に由来- ただRustでは「演算子名」ではなく「それが実行する操作」をトレイトの名前とする傾向がある
- e.g.,
PlusではなくAdd、StarやAsteriskではなくDeref
- e.g.,
- =>
Tryがこの操作に対する一番良い名前に見える
特になし