書籍「Rustの練習帳」を写経しながら読んでみた

前回、Rustを使ってPasoRiを扱いました。
usbipd-winによりRC-S300をWSL2で認識させ、WSL2環境にてpcsc-rustを使ってFeliCaのIDmを読み出してみた - メモ的な思考的な

ただ、参考にした記事をなぞりながら実装した感があったことから、もう少しRustの理解を深めたいと感じました。

 
そこで、

  • 写経することで、Rustの理解が深められる
  • できれば O'Reilly Online Learning で読める
  • 概念も学びたいので、できれば日本語ベースが嬉しい

という方向で何かないか探してみたところ、書籍「Rustの練習帳」がありました。
Rustの練習帳 - O'Reilly Japan

 
内容を見たところ

  • 既存のコマンドラインツールをRustで書く、というのはRustの書き方に慣れるのに良さそう
  • テストコードがある
  • 日本語に引っかかりや違和感がない

より、この本を写経するのが良さそうと感じました。

そこで、書籍「Rustの練習帳」を写経しながら読んでみたことから、その時のメモを残します。

 
目次

 

環境

  • WSL2
  • Rust 1.83.0
  • RustRover 2024.2.4

 

写経の方針

書籍は

  1. ベースとなったツールの機能やRustで書く時に実装が必要そうな関数のシグネチャなどが解説される
  2. 解説を元に、自分でロジックを考えて書く
  3. 解答例が示され、その意味が解説される

を繰り返す、という構成でした。

そのため、Rustの勘所が分かってない自分にとっては、2.に時間がかかりそうな上、その時間をかけても適切に書くのは難しそうでした。

そこで、「今回はRustの書き方に慣れることが重要なので、2.の自分でロジックを考えるところには注力しない」こととしました。

 
また、書籍の clap クレートのバージョンは 2 でした。

一方、書籍の付録Aやサポートサイトにある通り、 clap の3系以降ではAPIに変更が入ったようでした。そのため、書籍通りに写経したとしても、最新の clap の使い方とは異なりそうでした。

そんな中、英語版を O'Reilly Online Learning で確認したところ、 March 2024 Update という記載がありました。Rustやクレートのバージョンを確認すると

  • Rust 1.76.0
  • clap 4.5.0

と記載されていた上、 clap 4.5.0 の derive pattern での実装も紹介されていました。

これらより、今回どれを参照して写経するか迷いましたが

  • Rustの書き方に慣れるのが重要なため、違和感のない日本語で学ぶのが一番適切そう
  • clap クレートの最新版は、必要になったら学び直せば良い

と考えて、日本語版の書籍を clap 2系で写経することにしました。

 

感想

読み始めてからほぼ毎日写経し、最後の章まで実装しきったことで、Rustの書き方に慣れることができました。

特に

  • Result や Option の利用
    • ?unwrap
  • Ok や Err
  • map系の利用
    • map
    • map_err
    • filter_map
    • flat_map
  • パターンマッチングの利用
    • match
    • if let
  • 型変換
    • From::from
    • into()

などが書籍の中で繰り返し登場するため、写経していく中で慣れていき、最終的には読めるようになりました。

また、今回は「自分でロジックを考えること」は注力しなかったため、分からなくなったら都度調べることができました。これはRustの書き方に慣れていない自分にとってはちょうどよい進め方になりました。

 
これより、当初想定していた目的を達成できました。著者や翻訳者のみなさま、ありがとうございました。

 

写経をしていて、主に環境面で詰まったところ

書籍の内容とは異なるところ、特に手元の環境面で詰まったことについても記載しておきます。

 

WSL環境だと、catの実行結果がターミナルによって異なる

これは自分の環境構築がうまくいってないせいな気がしますが...

あるターミナルでは、 cat でファイルを複数指定したときにこんな結果になりました。

$ cat tests/inputs/fox.txt tests/inputs/spiders.txt
The quick brown fox jumps over the lazy dog.
Don't worry, spiders,
I keep house
casually.

 
一方、別のターミナルでは、 Don't の行が前の行とくっついてしまいました。

$ cat tests/inputs/fox.txt tests/inputs/spiders.txt
The quick brown fox jumps over the lazy dog.Don't worry, spiders,
I keep house
casually.

 
当初このことに気づかず、「実装はうまくできているはずなのに結果が違う...」と悩んでしまいました。

気づいてからは書籍と同じ挙動のターミナルを使うようにしました。

 

3章の mk-out.sh の結果が書籍のリポジトリと異なる

書籍のリポジトリにはテストデータが置いてあります。また、リポジトリにあるシェルスクリプトを使ってテストデータを作ることもできます。

ためしに3章のシェルスクリプト手元の環境で実行したところ、以下のように昇順で番号が振られました。

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

 
一方、書籍のリポジトリchap_v2 ブランチでは、ファイルごとに番号が振られていました。
https://github.com/kyclark/command-line-rust/blob/clap_v2/03_catr/tests/expected/all.n.out

 
どちらが正しいのか分からなくなったため、同じように写経していた方のリポジトリを確認したところ、自分と同じように通し番号となっていました。
https://github.com/nukopy/rust-renshu-cho/blob/main/cat/tests/expected/all.out

 
そこで、「自分の結果は正しそうだけど自信がない。そのため、 mk-out.sh などのテストデータを生成するシェルスクリプトを実行するのではなく、テストデータを書籍のリポジトリからまるっとコピーして動作確認したほうが良さそう」と考えました。

以降の章も、テストファイルはコピーすることにして進めました。

 

RustRoverの設定で行末のタブ文字が消されてしまう

今回の写経はRustRoverを使いました。
RustRover: JetBrains の Rust IDE

ただ、RustRoverの設定を「行末のタブ文字を消す」としていたため、テストファイルを開いて編集してしまうと、行末のタブ文字が消されてしまいました。

そこで、 Editor > General > On Save まわりの設定を見直しました。

 
ちなみに、テストコードを流したときにdiffは出るのですが、手元の環境だと

├── diff: 
│   ---   orig
│   +++   var
│   @@ -9 +9 @@
│   -     5
│   +     5   

のように表示され、「タブ文字の有無でdiffが出ている」が分かりづらい状況でした。

 

O'Reilly Online Learningだと b'\n' が B'\N' になっている箇所もあった

今回 O'Reilly Online Learning を使って写経していたのですが、

file.read_until(B'\N', &mut buf)?; 

のような表記となっている箇所がありました。

例えば、11章では以下のスクリーンショットのようになっていました。

 
ただ、書籍の紙版では b'\n' になっていたので、O'Reilly Online Learning だけかもしれません。

 

mapの引数における ; の有無

これは写経誤りにより発生したものです。

ふだん ; なしでも動く言語を書いているせいか、今回の写経でも ; が抜けてしまうことがよくあり、RustRoverに指摘されました。

その中で「なるほどー」となったのが、mapの引数における ; の有無でした。

 
例えば ; のない

let without_semicolon = [1, 2, 3].map(|v| {
    v + 1
});

と、 ; のある

let with_semicolon = [1, 2, 3].map(|v| {
    v + 1;
});

println! すると結果が異なりました。

fn main() {
    let without_semicolon = [1, 2, 3].map(|v| {
        v + 1
    });
    let with_semicolon = [1, 2, 3].map(|v| {
        v + 1;
    });

    println!("{:?}", without_semicolon); // => [2, 3, 4]
    println!("{:?}", with_semicolon);    // => [(), (), ()]
}

 
このように ; ありの状態でもコンパイルを通ってしまうので、しばらく詰まることもありました。

その結果、「 ; なしにすることで値を返す」への意識が大事と実感できたのでした。

 

写経結果

GitHubに上げました。
https://github.com/thinkAmi-sandbox/shakyo_command_line_rust

プルリクはこちら。
https://github.com/thinkAmi-sandbox/shakyo_command_line_rust/pull/1