Open19

[メモ] Webアプリ開発で学ぶRust言語入門 1〜3章

ki504178ki504178

1.1 Rust での開発の準備

rustup インスコ

ローカル環境に何もインストールしたくないタカ派(病気)なので、
rustup を Dockerイメージ使って、
VS Code + Remote Container でやってみる。

ローカル環境は以下の通り。

  • Mac M1 Air: 12.6
  • VS Code: 1.17.2

イケそう

root@65a0914dda25:/workspaces/rust_web# rustup -V
rustup 1.24.3 (ce5817a94 2021-05-31)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.53.0 (53cb7b09b 2021-06-17)`
root@65a0914dda25:/workspaces/rust_web# rustup update stable
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
713.2 KiB / 713.2 KiB (100 %) 671.4 KiB/s in  1s ETA:  0s
info: latest update on 2022-09-22, rust version 1.64.0 (a55dd71d5 2022-09-19)
info: downloading component 'cargo'
  6.4 MiB /   6.4 MiB (100 %)   3.1 MiB/s in  2s ETA:  0s
info: downloading component 'clippy'
info: downloading component 'rust-docs'
 18.8 MiB /  18.8 MiB (100 %)   2.0 MiB/s in  9s ETA:  0s
info: downloading component 'rust-std'
 27.4 MiB /  27.4 MiB (100 %)   4.2 MiB/s in  8s ETA:  0s
info: downloading component 'rustc'
 54.2 MiB /  54.2 MiB (100 %)  21.4 MiB/s in  2s ETA:  0s
info: downloading component 'rustfmt'
info: installing component 'cargo'
  6.4 MiB /   6.4 MiB (100 %)   5.5 MiB/s in  1s ETA:  0s
info: installing component 'clippy'
info: installing component 'rust-docs'
 18.8 MiB /  18.8 MiB (100 %)   1.4 MiB/s in 13s ETA:  0s
info: installing component 'rust-std'
 27.4 MiB /  27.4 MiB (100 %)   4.8 MiB/s in  5s ETA:  0s
info: installing component 'rustc'
 54.2 MiB /  54.2 MiB (100 %)   6.1 MiB/s in  8s ETA:  0s
info: installing component 'rustfmt'

  stable-x86_64-unknown-linux-gnu installed - rustc 1.64.0 (a55dd71d5 2022-09-19)

info: checking for self-updates
info: downloading self-update
root@65a0914dda25:/workspaces/rust_web# rustc -V
rustc 1.53.0 (53cb7b09b 2021-06-17)
root@65a0914dda25:/workspaces/rust_web# cargo -V
cargo 1.53.0 (4369396ce 2021-04-27)

Hello, world

cargo で作成したプロジェクトのルートに移動してから run する必要あり。

root@65a0914dda25:/workspaces/rust_web# cargo new hello_world --bin
     Created binary (application) `hello_world` package
root@65a0914dda25:/workspaces/rust_web# cargo run
error: could not find `Cargo.toml` in `/workspaces/rust_web` or any parent directory
root@65a0914dda25:/workspaces/rust_web# cd hello_world/
root@65a0914dda25:/workspaces/rust_web/hello_world# cargo run
   Compiling hello_world v0.1.0 (/workspaces/rust_web/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 7.39s
     Running `target/debug/hello_world`
Hello, world!

その他のツール

  • clippy による Lint

Dockerイメージ使ったからかすでに入ってた。サンプル通り Lint も効いた。

root@65a0914dda25:/workspaces/rust_web/hello_world# rustup component add clippy
info: component 'clippy' for target 'x86_64-unknown-linux-gnu' is up to date
  • format によるフォーマッター

フォーマッターも入ってた。

エディタの準備

誤字

  • Visual Studio Code は以下のサイトからのダウンロートできます
    • Visual Studio Code は以下のサイトからダウンロードできます
ki504178ki504178

2.1 変数とデータ型

データ型

  • 配列型

ほぇ〜。Rust の配列って固定長なのか。
可変長を使う場合はベクター型を使うと。

  • 文字列

文字列も既存メジャー言語たちとだいぶ違う感じなんだなぁ。

let message = "hello, world"; // 文字列スライスなので &str だからデータ長固定で結合したりできない
let message = String::from("hello, world"); // 可変長文字列として結合したりできる
ki504178ki504178

2.2 関数の実装

関数

Rust だと関数の戻り値なしは空のタプル () という特殊な値であり、構造体の一種である。

  • 「式」と「文」

お!RustもKotlinみたいに if が式で良き。
あと、;付けると文、;無しだと式になるけど、
関数の最後に return する時に式で書くみたいなのは慣れてないとぱっと見わかりづらそう。

(´-`).。oO(サンプルコードがページ跨ぎになってちょっと見づらいのがちょこちょこあるな、あとシンタックスハイライトされてるともっと分かりやすくなりそうだなぁ、電子書籍になったら修正されてると嬉しいなぁ)

2つのエラー

  • ? 演算子

    • 実行結果が Result の Error だった場合に、即時 return とかしたい時に使うやつ。await と組み合わせてよく使いそう。
  • unwrap/expect

誤字

  • また、特定にメッセージを出力してから
    • また、特定メッセージを出力してから

コメント

ドキュメンテーションコメント(///)にMarkdown使える、実装例を書いておくと cargo test でテストとして実行してくれるとか便利ね。

ki504178ki504178

VS Codeでサンプルとか実装する時にフォーマッターとかリンターをコード保存時に適用したくなってきたので、
以下を参考に settings.json に追加
https://zenn.dev/23prime/articles/74cda5a096a3b3#vscode-の設定を書く

settings.json
  "[rust]": {
    "editor.formatOnSave": true
  },
  "rust-analyzer.checkOnSave.command": "clippy",
  "rust-analyzer.checkOnSave.extraArgs": ["--", "-A", "clippy::needless_return"]

参考記事にあった parmeterHints は無くなってたっぽいので除外。
↑のコメントの中で式リターンが個人的にまだ馴染まないので return XX; のLintを追加しておく。

コマンドでLintする時に忘れそうなので一旦オプションつきコマンドメモ
cargo clippy -- -A clippy::needless_return

ki504178ki504178

2.3 制御構造

パターンマッチ

っぱパターンマッチが柔軟だと条件分岐が綺麗に書けていいなぁ

ki504178ki504178

2.4 所有権による安全性

ここらへんRustって感じする(小学生並の感想

所有権

変数を別の変数に束縛した場合、元となる変数からポインタ(メモリのアドレス)データ長のみコピーされ所有権はが移る。

ただし、Copy トレイトが実装されている型は値がコピーされるので、
別の変数に渡した後も使える。

i32はCopyトレイト実装されてるのでOK
fn main() {
    let a = 100;
    let _b = a;
    println!("{}", a); // 100
}

ライフタイム

ライフタイム注釈(&'static strみたいに使う)でコンパイラに変数のスコープを明示できる。
以下の例は文字列スライスで宣言してるのでプログラムにハードコードされ、
static ライフタイムを指定してるのでプログラム実行中は有効になる。

プログラム実行中有効
fn hello() -> &'static str {
  "hello"
}
ki504178ki504178

あれ?rustup update stableしたけど最新版になってない?

root@65a0914dda25:/workspaces/rust_web/hello_world# rustup update stable
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'

  stable-x86_64-unknown-linux-gnu unchanged - rustc 1.64.0 (a55dd71d5 2022-09-19)

info: checking for self-updates
root@65a0914dda25:/workspaces/rust_web/hello_world# rustc -V
rustc 1.53.0 (53cb7b09b 2021-06-17)

調べたらオーバーライドしてみそって書いてあったので試したけど、
rustc -Vで謎のメッセージが出力されるな・・・

root@65a0914dda25:/workspaces/rust_web# rustup override set 1.64.0
info: syncing channel updates for '1.64.0-x86_64-unknown-linux-gnu'
info: latest update on 2022-09-22, rust version 1.64.0 (a55dd71d5 2022-09-19)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
 27.4 MiB /  27.4 MiB (100 %)  25.0 MiB/s in  1s ETA:  0s
info: downloading component 'rustc'
 54.2 MiB /  54.2 MiB (100 %)  26.2 MiB/s in  2s ETA:  0s
info: downloading component 'rustfmt'
info: installing component 'cargo'
  6.4 MiB /   6.4 MiB (100 %)   5.4 MiB/s in  1s ETA:  0s
info: installing component 'clippy'
info: installing component 'rust-docs'
 18.8 MiB /  18.8 MiB (100 %)   1.5 MiB/s in 12s ETA:  0s
info: installing component 'rust-std'
 27.4 MiB /  27.4 MiB (100 %)   4.9 MiB/s in  5s ETA:  0s
info: installing component 'rustc'
 54.2 MiB /  54.2 MiB (100 %)   6.1 MiB/s in  8s ETA:  0s
info: installing component 'rustfmt'
info: override toolchain for '/workspaces/rust_web' set to '1.64.0-x86_64-unknown-linux-gnu'

  1.64.0-x86_64-unknown-linux-gnu installed - rustc 1.64.0 (a55dd71d5 2022-09-19)

root@65a0914dda25:/workspaces/rust_web# rustc -V
<jemalloc>: MADV_DONTNEED does not work (memset will be used instead)
<jemalloc>: (This is the expected behaviour if you are running under QEMU)
rustc 1.64.0 (a55dd71d5 2022-09-19)
root@65a0914dda25:/workspaces/rust_web# cargo -V
cargo 1.64.0 (387270bc7 2022-09-16)

M1(Arm)のせいっぽいけど cargo run もできたし、1.58から使えるようになったていう変数埋め込みもできたからええか。

fn main() {
    let a = 100;
    let _b = a;
    println!("{a}"); // 引数じゃなくて文字列に直接指定できる!!式(`a + a`とか)はまだ無理
}
ki504178ki504178

2.5 データ構造

Java育ちだからクラス無いと不思議な感じ。

構造体

構造体にそのまま変数渡した場合って束縛はどうなるんだろうと思って試したら、
Copy トレイトありなしの話と同じだった。

メソッド、関連関数

  • 構造体にメソッドなどを生やす場合は impl 構造体名 で定義する
  • メソッドは構造体のインスタンスを前提としてるため、かならず第一引数に &self を受け取る関数となる
  • インスタンスに紐づかない(第一引数として &sefl を受け取らない)関数として 関連関数 を定義することができ、Rustの慣例として インスタンスを生成する new 関連関数を生やすことが多い。
ki504178ki504178

2.6 async/await

TypeScriptとそんなに変わらなかったのでスッと入ってきた。

  • async

誤字

  • Futrureトレイトを返す糖衣構文です。
    • Futureトレイトを返す糖衣構文です。

RustではJavaScriptでいうPromiseをFutureトレイトで返す。

  • await

JavaScriptと違ってプロパティアクセスのような記法になってる。
?オペレータとの相性を考慮してそう。

  • tokio

Rustで人気の非同期処理ランタイムライブラリ。名前からして農業に強そう。
Rustは歴史的な背景(調べてない)から非同期ランタイムを標準で持ってない。

ki504178ki504178

2.7 クレートとモジュール

最近 TypeScript ばっか書いてるから、ここらへんの公開制御が細やかにできるのうらやまC

  • パッケージ

機能を提供する単位。必ず1つ以上のクレートをもつ。
ルートディレクトリのCargo.tomlでパッケージの設定や依存パッケージを定義する。

  • クレート

コンパイル単位。ライブラリクレートとバイナリクレートの2種類がある。
ライブラリクレート:他のパッケージに公開するライブラリ週で、公開モジュールはsrc/lib.rsにまとめる必要あり。
バイナリクレート:src/main.rsを起点に実行可能なバイナリを生成して main 関数を実行する。複数定義する場合はsrc/binは以下のファイルがそれぞれの起点となる。

  • モジュール

コードをグループ化し、ファイル分割する。
pubでモジュール・関数を 1つ上のスコープに公開 できる。pubなしは同一スコープのみ利用可。

ki504178ki504178

テスト

デフォルト(Cargo)でテスト環境提供されてるの良き。

単体テスト

単体テストのコード実行しようとしたけど、main.rsと別ファイルにしたら対象外になって実行されなかった。
rust-analyzer の指摘通りに テスト.rs のテスト対象関数を pub にして、main.rsmod テストファイル名; を追加したらテスト対象として実行された。

  • テストコードはテスト対象のモジュールと同じファイルに実装する

    • これテストパターン多くなってきたり、テスト共通の関数とか生えまくってきた時に見づらくなったりしないのかな?
  • cfg(test) 属性

    • 環境に応じてコンパイルを切り替える属性風マクロ。テスト時のみコンパイルなどを定義できる。
      単体テストのコードはまとめて以下のようにしておくと便利
      #[cfg(test)]
      mod tests {
        // ...
      }
      
  • test 属性

    • テストケースとして認識させるためにテストメソッドに設定する。
      #[test]
      fn test_case() {
      
      • JUnit の @Test アノテーションみたいなもんやね
      • あとメソッド名に日本語使えた。fn add_1足す1は2()みたいな感じで。
    • 引数無し、戻り値は空タプルか Result のみ許容。Reuslt 返せるので ? オペレータも使える
  • アサーション

    • assert!マクロ:引数が true
    • assert_eq!マクロ:2つの引数が等価であるか。引数の型はPartialEqを実装している必要あり

ドキュメントテスト

これは一番使われそうなパターンをドキュメントテストとして書いて、
他の細かいパターンがあれば↑の単体テストを別で書いてくスタイルが良さそう。

ただサンプルコードの構成的にどこにどのファイル置いてるのかピンとこなくて、
最初に作ったhello_worldmain.rsと同じ階層にファイル配置してみてDocument Test実行してもno library targets found in package 'hello_world'になっちゃう。。。

結合テスト

ライブラリクレートで外部公開する関数をテストする際に、srcと同階層に tests ディレクトリ作成して外部呼び出しテストを実施できる。

ki504178ki504178

2.9 よく使うライブラリ

Err をより扱いやすくする anythrow/thiserror

エラーハンドリングを簡易にするたために活用できる

JSON を扱う Serde

Rust でJSON(YAML/TOMLなどもOK) を使う場合のシリアライズ/デシリアライズライブラリ。
Webフレームワークでよく使われている。

ki504178ki504178

3.1 axum とは

axum とは

2022年現在、以下のような選択肢があるが、tokioとの親和性やマクロを使わない設計などに着目し、API実装にはaxumを使う。

  • 古参
    • Rocket:シンプルな書きっぷり・高速・セキュアを目指している。記事多め。
    • actix-web:http2, Websocketなど対応範囲広い、一時期は techempowerという会社の比較で、全フレームワークで最も高いスコアを出していたこともあるほどハイパフォーマンス
  • 新参
    • warp:Fileter という概念で実装する独特なやつ。http2, Websocket など対応してる。後発なので古参よりは知名度控えめ
      • 少し前から使い出したターミナル Warp と名前一緒、こっちも Rust で書かれてるやつ。個人的(これは私)にオヌヌメ

[参考]insomnia のすすめ

Postman くらいしか使ったことなかったけど使ってみる。
https://insomnia.rest/download
ちな brew install insomnia でもイケる。

あとちらっとPostmanとの比較記事とか無いかなぁと調べた感じ、
MacならPostman, insomniaではなく Paw(初見)もオヌヌメだよって話もあった。
https://rapidapi.com/blog/insomnia-vs-postman-vs-paw/

ki504178ki504178

3.2 環境構築

シンプルにAPIサーバー実装できて良き。

Hello, world

Cargo.toml 修正した後、VS Codeで書く時に use の補完とか効くかなってとこを試したかったので、
先に cargo build でライブラリインストールしようとしたが、
crates.io からFetchしてくるところで、必ず 33% くらいで Kill されちゃってインスコできなかった。
Dockerが怪しそうだったんで、とりあえず Docker Desktopの設定で、Swapを2GB→3GBにしたらFetchは成功した。
ただ、ライブラリたちのビルド走ってるところでまた Kill されたので、
再度 cargo build したらイケた。
ただライブラリのビルドめっちゃ時間かかった(多分10分以上)、あと cargo run でサーバー立ち上がるまで数分かかる(白目)>多分Dockerが遅いから

そして rust-analyzer のチェックが VS Code で完了した後、
use の補完も効くようになった。

logging

ログレベル設定のコードでLintがWarning出してきたので、Lintの指摘通り修正したら出なくなった。

- let log_level = env::var("RUST_LOG").unwrap("info".to_string());
+ let log_level = env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
ki504178ki504178

3.3 テスト

テスト実装するとAPI実装してる感爆上げ。
あと、#[cfg(test)] みたいに環境によってコンパイルされなくなるようなやつ良いよね。TypeScriptにも欲しい。

余談だけど、VS Codeで実装中の時に、コード保存時だけでなく常にLintしてくれるオプションは無いのだろうか😑

ki504178ki504178

3.4 Todo 情報を保存する

なんかカタカナ以外の表記で、1文字ごとにスペースが入ってる表記がちょこちょこあるの何故だろう?フォントのせいかな?

TodoRespository を仮実装

  • RepositoryErrorNotFound に付与してる error はなんの型使ってるのか分からなかった。stdのやつでよいのだろうか?
  • よく使う derive(...) を共通化したいな

TodoRepositoryForMemoery の実装

  • 関係ないけど、impl実装する時の入力補完イイね!!

  • Arc<RwLock<>> はスレッドセーフに書き換えを行い時に使うパターンの一つ。敷衍参照のときは複数スレッドで教諭でき、可変参照のときのみ RwLock がスレッドを一つに制限してくれる。

  • リポジトリの共有

誤字

  • ジェネリクエス T: TodoRepository で指定しています。
    • ジェネリクス T: TodoRepository で指定しています。

Todo をメモリに保存する

impl TodoRepository for TodoRepositoryForMemory に実装する。

  • create メソッド

payload.text.clone() してるところで↓のWarning出たので、指摘通り .clone() 除外したら消えた

  • find メソッド

map.(|todo| todo.clone())で↓のWarning出たので、指摘通りcloned()にしたら消えた

  • all メソッド

こっちも↑のfindメソッドと同様だった。

  • update メソッド

メソッド戻り値の型でErrorが出たが、pub trait TodoRepositoryの方の戻り値型のジェネリクスを() から Todo に変更したら解消された。

let text = payload.text.unwrap_or(todo.text.clone());で以下のWarningが出たので、指摘通り let text = payload.text.unwrap_or_else(|| todo.text.clone());に変えたら解消された。

ki504178ki504178

3.5 HTTP リクエスト

SpringのControllerのアノテーションみたいに、それぞれのファイルでルーティングやHTTPステータスコード決められるやつもあるのかなぁ。

ハンドラー

main.rsget(all_todo::<T>)のところで以下のエラーが表示された。

単純に handlers.rsall_todo の戻り値型に impl つけ忘れだった。

create_app のテスト実装で、以下の部分でWarning出たので、指摘通りunwrap_or_else(|_| panic!("cannot convert Todo instance. body: {}", body));にしたら解消された。他の expect も同様。

あと build_todo_req_with_jsonbuild_todo_req_with_empty の引数の順番で、
pathとmethodが逆になってるのなんか理由あるのかな?と思い合わせてみたけど特に問題なさそう。

DELETE: /todos/:id Todo 情報の削除

時々出てくる .map(|_|, xx) の第一引数は TypeScript の Array.mapの第一引数みたいなもんかな?

ki504178ki504178

3.6 バリデーションの追加

ここまでで利用しているクレートのVerが 0.X.X なのが半分くらいあるのを見て、
Rustはやっぱり後発の言語なんやなぁと思った。

後全然関係ないけど、VS Code + Remote Container 遅いからちょこちょこ待ちガイル状態になるなぁ。

validator

シンプル、かつカスタマイズもサクッとできそうで良さそう。

CreateTodo の validate

お、結構複雑な impl が出てきたぞ。落馬しないようにしっかり読み込もう。

  • #[async_trait]

Rust のトレイトメソッドでは本来 async は使えないが、async-trait というパッケージのマクロでトレイト内で async を利用可能とすることができる。
FromRequest を実装する側も同様にこの属性風マクロを宣言する必要がある。

後関係ないけど「ぞくせいふうまくろ」で一発目に出てきた変換が「属性風磨黒」という厨二病みたいな名前。

  • ジェネリクス T, B

結構複雑だと思いきや Json::<T>::from_request(req) とするためのトレイト境界宣言。

ここの where の : が色々なパターンの表現混在しててちょっと混乱しかけた

where
    T: DeserializeOwned + Validate,
    B: http_body::Body + Send,
    B::Data: Send,
    B::Error: Into<BoxError>,

誤字?

  • HttpBody が http_body::Body のことになります。
    • HttpBody が http_body::Body になります。

示してるコードが違いそう?

Json の実装の一部が以下の〜 のところの直後に出てくるコードはその↑のコードと同じだけど、
これは文脈からすると from_requestメソッドの1行目らへん?

  • type Rejection

FromRequest を実装するために必要な関連型。IntoResponse を実装する必要あり。

  • from_request の実装

これ専用の属性風マクロとかありそうだと思ったけどそんなことないのかな?
Springの Controller @Validated アノテーションみたいな。