Rust でエラー型に Clone が実装されていてほしい

これは物申す系の話とかではなく、単なる愚痴。 現状については現状セクション参照。

概要

  • エラー云々以前に、ユーザが後から (Clone 等、自前でない trait の)実装を追加できない[0]以上、ライブラリ作者はできるだけ基本的な trait 群を derive しておくべきである。
  • エラー型のオブジェクトを clone したい場合が存在する。
  • error_chain crate でエラー型を用意すると、現状(0.11.0 ( リリース)時点)では derive(Clone) できない。

以上のような理由で、つらい。

エラーを clone したい状況

処理の完了や致命的な失敗の後にも、 Result を返す関数の呼び出しに制約をかけたくない場合

たとえば XML パーサである xml-rs crate 等が該当する。 xml::reader::EventReader::next() は、(致命的な)パースエラー等があると Err を返すが、エラーが返された後も next() 自体は制限なく何度でも呼び出すことができる。

If returned event is XmlEvent::Error or XmlEvent::EndDocument, then further calls to this method will return this event again.

xml-rs 0.6.1 のドキュメント より。 強調は引用者による。

このような場合、最後のエラーを複数回返す必要が出てくるため、当然エラー型は Clone を実装していてほしい。

以前、私が xml-rs を参考に書いた fbx_direct crate では、エラーとして I/O エラー (std::io::Error) が有り得たため、自前で Clone を実装することになってしまった。 Qiita の記事『Rustで std::io::Error をcloneしたいとき - Qiita』はこのときの副産物である。

完了や失敗の後の関数呼び出しに制約をかけるというのもひとつの選択で、たとえば futures-rs などはその選択を採っている。

Once a future has completed (returned Ready or Err from poll), then any future calls to poll may panic, block forever, or otherwise cause wrong behavior. The Future trait itself provides no guarantees about the behavior of poll after a future has completed.

futures-rs 0.1.16 のドキュメント poll 関数の項目の Panics セクションより。 強調は引用者による。

futures-rs の Future trait 自体は各ライブラリ開発者が各々の型に対して実装しうるものであり、エラー型も様々だから、実装を単純にするためにこのように定めたのであろう。

エラー型を致命的でない警告に対して使いたい場合

コンセプト (サンプルコード)

致命的でない警告をエラーとして扱うかどうかユーザに委ねたい場合などがあり、この場合警告は Error trait を実装していると良い。 コード例を書いたので参照されたい (playground へのリンク)。

                // Buy only clean and not broken items.
    assert_eq!(
        Err(ShoppingError::Warning(ShoppingWarning::Dirty(2))),
        buy(1, 2, |w| Err(w))
    );
          
商品が汚れていたり壊れていたら買い物をやめる例 (ソースコード より抜粋, playground へのリンク)。
                // Buy not broken items, allow dirty items.
    assert_eq!(
        Ok(2),
        buy(1, 2, |w| {
            if let ShoppingWarning::Broken = w {
                Err(w)
            } else {
                Ok(())
            }
        })
    );
    // Buy not broken items, allow dirty items.
    assert_eq!(
        Err(ShoppingError::Warning(ShoppingWarning::Broken)),
        buy(2, 2, |w| {
            if let ShoppingWarning::Broken = w {
                Err(w)
            } else {
                Ok(())
            }
        })
    );
          
商品が汚れていても買うが、壊れていたら買い物をやめる例 (ソースコード より抜粋, playground へのリンク)。
                // Buy not broken items, allow dirty items.
    // Log troubles.
    let mut troubles = Vec::new();
    assert_eq!(
        Err(ShoppingError::Warning(ShoppingWarning::Broken)),
        buy(2, 2, |w| {
            troubles.push(w.clone());
            if let ShoppingWarning::Broken = w {
                Err(w)
            } else {
                Ok(())
            }
        })
    );
    assert_eq!(
        troubles,
        vec![
            ShoppingWarning::Dirty(5),
            ShoppingWarning::Broken,
        ]
    );
          
商品が汚れていても買うが、壊れていたら買い物をやめ、それはさておきトラブルを外部に記録しておく例 (ソースコード より抜粋, playground へのリンク)。

警告のエラー型が Clone を実装していてほしいのは、まさに最後の例のような場合である。 エラーをどこかに記憶(複製)したのち確認したい場合、 clone() できてほしい[1]し、 assert_eq!() 等でテストをしたい場合には Clone 以外にも PartialEq なども必要になるから、 Clone に限らず基本的な trait はとにかく実装しておいてほしいということである。

具体例: fbxcel crate (自作)

先述の fbx_direct crate とは別に(というか改良して)、 fbxcel という crate を開発していて、こちらでは、致命的ではないがおかしいデータについて Warningで警告を発するようになっている。 単なるロギングではなく、こうして専用の型のついたオブジェクトを経由させることで、このライブラリを利用するアプリケーションからも警告履歴を利用可能となる。

fbxcel が発した警告を、 fbxcel を利用するアプリケーションが自由に整形して表示できる。
fbx-tree-view (GUI のビューア)で fbxcel が発した警告を表示した例 (画像下部)

具体例: fsck-xv6 (自作)

いつぞやの OS の授業の課題で xv6 のファイルシステムの validator (fsck-xv6) を実装したことがあり、このときもファイルシステムエラーを表現するための型に Error trait を実装した。

こちらは一般的な意味での「エラー」ではあるが、ファイルシステムの妥当性検証という性質上、ファイルシステムのエラーがそのままプログラムの継続不可能を意味するわけではない。 よって、複数のエラーが発生すればそれらを全て列挙したり、或いは Vec 等のコンテナに溜めたりといった用途を想定することになる。

                  let errors = fs::validate(target_file)
        .expect("Critical error happened and validation cannot be proceeded");
    if errors.is_empty() {
        println!("No errors detected.");
    } else {
        println!("Errors detected:");
        for err in errors {
            println!("{}", err);
        }
    }
            
src/main.rs, main 関数内より抜粋
              pub fn validate<P: AsRef<Path>>(path: P) -> io::Result<Vec<Error>> {
    let path = path.as_ref();
    let mut reader = BufReader::new(File::open(path)?);

    let mut errors = vec![];
            
src/fs/mod.rs, トップレベルより抜粋
              /// Validation error.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Error {
    BitmapMismatch { target: BlockIndex, expected: bool },
    DataBlockReferenceConflict { block: BlockIndex },
    DirectoryWithoutDot { inode: Inode },
    DirectoryWithoutDotdot { inode: Inode },
            
src/fs/error.rs, トップレベルより抜粋
エラー型の実装

この設計は fbxcel のエラー処理の発想をそのまま使ったものであり、ゆえに結局警告をエラーとして使いたいのかそうでないのかよくわからない設計になっている。 今の私が同じような目的のコードを書いたら、最初の例のように特定の警告にフックをかけて処理を中断できるように作るはずである。

現状

ここまで熟々と書いてきたのは、つまるところエラーを clone したいというだけの話である。 現状でそれを妨げる要因は、以下のようなものである。

std::io::Error が Clone を実装していない

悲しいことに、そうなのである。

issue が立ってはいるが進展の様子はなく、当面は Qiita に書いたように誤魔化しつつやっていくしかない。

error-chain trait がエラー型に対する derive をサポートしていない

error-chain crate は、エラー型や関連する諸々を用意する際のボイラープレートを減らすためのライブラリである。 イマドキ手書きの温かみのあるエラー型を書いたりしないよね、 error-chain は人権だよねという空気がある[要出典]が、残念ながら error-chain で定義したエラー型は Clone を実装していないのである!!! ()

プルリクエストもあり議論は進んでいるようだが、今使いたい私にとっては重大な問題である[2]

参考リンク