🦀

[写経]Rustの練習帳 Chapter 3(その1) Result、deriveマクロ、Debugトレイト

に公開

教材

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

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

前回のページ:
https://zenn.dev/okani/articles/6b9716a458015f

3章に進む

まだ理解できていないところがあるけれども、ずっと2章で詰まっているよりも先に進んだほうがモチベーションを維持できるので、先に進むことにしよう。
つまづいたらまた戻ればいいや。

TDD での進め方を教えてくれるようだけど、リポジトリのクローンが面倒だったのでテストコードはすべて後回しにする🤪

catの振る舞い

書籍では、cat -n で複数のファイルを渡すと、ファイルを読む都度、行番号が1から振られると書いてある。

$ cat -n empty.txt fox.txt spiders.txt the-bustle.txt
1 The quick brown fox jumps over the lazy dog.
1 Don't worry, spiders,
2 I keep house
3 casually.
1 The bustle in a house
2 The morning after death
3 Is solemnest of industries
4 Enacted upon earth,̶
5
6 The sweeping up the heart,
7 And putting love away
8 We shall not want to use again
9 Until eternity.

が、手元のUbuntuだと行番号もファイルをまたいで連番で振られてしまっている。
これはBSD版catとGNU版catの違いなのだろうか?

BSD版catをLinux環境でお手軽に試せるのはできなさそうだし、このためにBSDの仮想環境を用意するのは面倒なので、今はやめておこう。
これから作成していくcatプログラムがBSD版の振る舞い前提だった場合はその時にまた考える🙃

3.2.2 ライブラリクレートの前に Result でつまづく

ライブラリとバイナリで役割を分けるという話が登場した。
こうやってお作法に触れるようになるとわくわくするね😊

TestResult がOk バリアントで常にユニット型() を返すのに対し、MyResult は任意の型に対するOk を返すことができます。

うーん、T は任意の型を指すことはわかるけれども、常にユニット型ってどういうことだっけ?
Result への理解をすっ飛ばしたことによるツケがはやくも登場してしまった。

ちょっと振り返ることにする。
2章で登場した TestResult :

type TestResult = Result<(), Box<dyn std::error::Error>>;

3章で登場する MyResult :

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

ふむ、確かに Result の第1引数に指定している型が () と T で異なっていた。
型の指定時に () とすると、常にユニット型の扱いになる、ということなのかな?
でも、そもそもこれって何の意味があったのだろう?

Claude Code くん、助けて!

おおー、なるほど、捨ててしまって構わない時にはユニット型を指定していた、ということか。
で、今回はテストコードではなく、通常のコードなので値を返したくなる状況だから、() ではなく T を指定して返却値を使えるようにしている、という違いが生まれているのだね。

改めて、以下のコードの意味を解釈する:

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

これはつまり、
任意の型Tを指定して利用するジェネリクスな MyResult 型の実体は、

  • 成功時:
    • 任意の型Tを返す
  • エラー時:
    • Error型のインターフェースを実装してさえいれば、どんな型でも返せる
    • スタック上ではなくヒープメモリに保管されている
      • なぜならば、Error型を実装している個々の型のサイズは不明&不定なので、ヒープ上で動的に確保する必要があるため

ということだと自分の頭で再整理できたぞ。
ちょっと知識を繋げられたという実感が持てたのは嬉しいな😄

3.2.2 ライブラリクレートに戻る

Result型の理解が進んだので、本筋に戻ることにしよう。

初めて登場したライブラリクレートはこちら:

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

pub fn run() -> MyResult<()> {
println!("Hello, world!");
Ok(())
}

改めて読んでみると、この run() での使用方法においては、MyResultの引数にユニット型を指定しているので、それはつまり戻り値を使わないということだから、実質的には TestResult と同じだね🎵

3.2.3 deriveマクロ、Debugトレイト

本文にも黒魔術が登場してきた!

#[derive(Debug)]
pub struct Config {
    //...
}

derive マクロ(https://oreil.ly/Lr8JE) では、Debug トレイト(https://oreil.ly/cEl5P) を追加して、構造体を表示できるようにします。

お便利機能という感じかな?
derive マクロと Debug トレイトの役割分担がわかっていないので調べることにする。
Gemini くんに質問した結果がこちら:

Debugトレイト deriveマクロ
位置づけ 「何ができるか」を定義 「どう作るか」を自動化
役割 その型を「デバッグ用の文字列」として出力可能にする トレイトの標準的な実装を、コンパイラに自動で書かせる
補足 主に println!("{:?}", config); のように、開発中に中身をパッと確認したい時に使われる 構造体の各フィールドを一つずつ取り出して文字列に変換するコードを手書きする必要がある場合、derive を使うとそのボイラープレート(定型文)をすべてスキップできる

Debug トレイトを引数に指定することで「こういうことしたいよ」を derive マクロに伝え、derive マクロは良い感じにメタプログラミングでコード生成してくれる、ということなんだね。

と、ここで新たな疑問が。
derive マクロは既知の標準的なトレイトが渡された場合はコード生成ができるとは思うけど、独自のトレイトを渡した場合、未知のものに対してどうやって適切なコードを生成するのだろうか?🤔

Gemini くんに追加で質問してみた。

あー、そういうことか!
derive は日本語に翻訳したら「導き出す」だったけど、確かに対応付けの管理をしている役割ならばその名前の通りの振る舞いだね。

もし自分だったら、XXXジェネレータみたいに何かを生成させるような短絡的な名前にしてしまいそうな気がするけど、抽象レベルを上げて「導き出す」という名前を付けられるってのは憧れるなー😆

Discussion