🦀

[写経]Rustの練習帳 Chapter 4(その1) FromトレイトからErrorトレイトへ

に公開

教材

書籍「Rustの練習帳」を使って、教養としての Rust を勉強。

https://www.oreilly.co.jp/books/9784814400584/

前回のページ:
https://zenn.dev/okani/articles/0f5039df081d09

立ちはだかる壁が高い

AIの力を借りながら何とか4章をやり抜いたものの、色々と壁にぶつかり苦労の連続で終わってしまった😵‍💫

4章で疑問に思った箇所やつまづいたところを振り返っていく。

単体テストと統合テスト

前の章でもテストコードは登場していたものの、それは統合テストという呼び名だった。
あれ、こうやってテストコードを書いてassert文でチェックするようなやつって単体テスト(Unitテスト)って言うのではなかったっけ?という感覚があり、少し引っかかっていた。

確認してみたところ、Rustの世界では、本体コードの中に埋め込むような形でテスト用コードを用意するのが単体テストという扱いらしい。

本体コードに埋め込んでいることのメリットとして、プライベート扱いで隠ぺいされている関数や構造体にもアクセスができる。
そのメリットはよく理解できたけれども、プロダクション向けの生成物にテストコードもセットで入ってくるというのは、こう、なんというか気持ちの悪さを感じるな。
…と思っていたら、アノテーションをつけておくことで通常のビルド時にはテストコードがコンパイル対象外になってくれるとのこと。なるほどねー。

str::parse()

戻り値の型をもとに、今回はusize型でのパースをすると推論してくれると。

なお、str::parse() が全知全能で全ての型に向けてどのようにパースすればよいかを知っているわけではない。
型パラメータで受け取った型は FromStr トレイトで規定されている from_str() を実装している前提なので、str::parse() としては単に from_str() の呼び出しをしているだけだった。

pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
    FromStr::from_str(self)
}

どうパースすればよいかを知っているのは、パース先となる各々の型になる。

match アーム

へー、こんなこともできるんだ。
じゃあ、もっと複雑な判定したいからと条件式の箇所を関数呼び出しに置き換えることもできたりするのかな、と思ってAIに質問。

あー、そうか、確かにそうかも。
いや、もしここにデシジョンテーブルっぽいものを用意したならば…
でも、ここでは bool を返さないといけない制約があるし…
…やっぱり複雑な場合分けを実現したかったならば、それは match の手前で判定したほうがよさそうだな🙂‍↕️

REPL

match で何ができるかを妄想していたけれど、こういったちょろっとした確認をするのにわざわざ cargo でプロジェクトを作って、というのが面倒くさいなと思ってやらずじまいになってしまう。

REPL みたいなものがあったらよいのだろうけれども、Rust の場合はコンパイラの役割や特性からしてREPL環境は提供されてないよね?

念のためにAIに確認したところ、ちょろっとした確認なら REPL ではないけれども Rust Playground を使うとよいよ、と教えてもらった。

おー、これは便利そう。

FromトレイトからErrorトレイト

型を変換するためのもの、として、 From::from() があるということは理解した。
うん、それは理解したのだけども…Err型との組み合わせが腑に落ちていないな。

let s = String::from("hello"); 

これは &str から String を生成しているというのはわかる。

独自に定義した構造体などに From トレイトを実装しておけば、ある型から自分の型へと変換させてあげることができるようになることもわかった。

でも、以下のErrを作る過程がまだよくわからない。

_ => Err(From::from(val));

AIに壁打ちしてみたところ、どうやら型推論が混じっているから自分には余計に難しく思えているみたいだ。

type MyResult<T> = Result<T, Box<dyn Error>>;

fn parse_positive_int(val: &str) -> MyResult<usize> {
    match val.parse() {
        Ok(n) if n > 0 => Ok(n),
        _ => Err(From::from(val)),
    }
}

上記の場合、裏でコンパイラは以下のように解釈するらしい。

  1. parse_positive_int() の戻り値のエラー型は MyResult<T> のエラー型である Box<dyn Error> だな
  2. だから、今回の Err(...) の中身は Box<dyn Error> でないとダメだ
  3. Err(...) の中身は From::from(val) で、 val は &str だ
  4. ということで、 impl From<&str> for Box<dyn Error> の from(&str) 実装を探すぞ
  5. 標準ライブラリ内に見つかったので、そのロジックを実行しよう

なお、標準ライブラリ内の実装はこうなっていた。

impl<'a> From<&str> for Box<dyn Error + 'a> {
    /// コメント文省略
    fn from(err: &str) -> Box<dyn Error + 'a> {
        From::from(String::from(err))
    }
}

ここをもう少し紐解きしてみる。

  1. 文字列スライス型の &str を渡されたら、まずはString型の from() でヒープメモリ上に格納されるString型へと変換する
  2. 生成された String を From::from() でさらに変換しにいく
  3. ここの From::from() は誰がロジックを持っているかというと、戻り値が Box<dyn Error + 'a> なので、同じく Box<dyn Error> 自身に定義されている
  4. Box<dyn Error> の From<String> 実装が実行される

その先はどうなってくるかというと以下になるみたいだ。
あ、黒魔術的な記載はいったんすべて無視して追いかけている。

impl<'a> From<String> for Box<dyn Error + 'a> {
    /// コメント文省略
    fn from(str_err: String) -> Box<dyn Error + 'a> {
        let err1: Box<dyn Error + Send + Sync> = From::from(str_err);
        let err2: Box<dyn Error> = err1;
        err2
    }
}

impl<'a> From<&str> for Box<dyn Error + Send + Sync + 'a> {
    /// コメント文省略
    fn from(err: &str) -> Box<dyn Error + Send + Sync + 'a> {
        From::from(String::from(err))
    }
}

Box<dyn Error> の from<String> 実装では、Box<dyn Error + Send + Sync> で定義されているはずの From::from(err: String) でさらに変換しにいく。

その中は以下。

impl<'a> From<String> for Box<dyn Error + Send + Sync + 'a> {
    /// コメント文省略
    fn from(err: String) -> Box<dyn Error + Send + Sync + 'a> {
        struct StringError(String);

        impl Error for StringError {}

        impl fmt::Display for StringError {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt::Display::fmt(&self.0, f)
            }
        }

        // Purposefully skip printing "StringError(..)"
        impl fmt::Debug for StringError {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt::Debug::fmt(&self.0, f)
            }
        }

        Box::new(StringError(err))
    }
}

このあたりはさっぱりわからなかったのでAIに全面的に解説してもらった。

ここでは、 StringError というString型を唯一のメンバにラップした構造体を定義している。
なんでわざわざラップした構造体を用意しているのかというと、String型は Error トレイトを実装していないので、そのままだと Error として扱えないから。
このラップした状態で Box を生成するのが目的。

で、Error トレイトを実装するからには、既定の関数を定義しないといけないはずなのに、この StringError は、なんと空の定義だけで終わっている。

impl Error for StringError {}

なぜ一見トレイト実装のルールを無視したかのようなコードが許容されるのかというと、 Error トレイトではデフォルトの振る舞いが実装されているから。
なので、個別実装していなくてもそのままで済ませられる。
トレイトではデフォルト実装のようなことができるのかー、と実例を通して知ることができた。

で、Error トレイトの定義は以下のようになっている。

pub trait Error: Debug + Display {
    // 省略
}

ここの 「: Debug + Display」 という記載は、Debug トレイトと Display トレイトも実装しないと Error トレイトとしては扱えない、というトレイトの継承関係が表されている。

そのため、先ほどのコードでは、以下の Display トレイトの実装コードと Debug トレイトの実装コードが登場した、ということになる。

impl fmt::Display for StringError {
    // 省略
}

impl fmt::Debug for StringError {
    // 省略
}

Display と Debug でトレイトが分かれているのは、きっと合理的な理由があるのだろうなとは思いつつ、今はスキップする。

これでようやく StringError をラップした Box<dyn Error + Send + Sync + 'a> 型を生成することができた。

from() の実装に戻る。

fn from(str_err: String) -> Box<dyn Error + 'a> {
    let err1: Box<dyn Error + Send + Sync> = From::from(str_err);
    let err2: Box<dyn Error> = err1;
    err2
}

err1 は Box<dyn Error + Send + Sync> 型で、これは スレッド間を移動できる(Send)、共有できる(Sync) という追加条件を満たすエラーである、ということらしい。
スレッド周りを勉強するのはまだ先だと思うので、今は踏み込まないでおく。

その厳しい条件付きエラーである err1 を Box<dyn Error> と追加条件なしのよく見る型の err2 へ代入してあげることで、追加条件が緩和してもらえることになるそうだ。

スタート時点は文字列スライスだった情報を、String にしてから Errorトレイトを実装したラップ構造体へと移したことにより、これでようやく Error として扱えるようになり、そのおかげで最後は Err として返せる処理が実現できた、ということになる😊

うーむ、From トレイトと Error トレイト、奥が深いのう!

Discussion