[メモ] Webアプリ開発で学ぶRust言語入門 1〜3章
Rust何もわからん状態からWebアプリ開発で学ぶRust言語入門を読んで気になったとことかメモしていく
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 は以下のサイトからダウンロードできます
2.1 変数とデータ型
データ型
- 配列型
ほぇ〜。Rust の配列って固定長なのか。
可変長を使う場合はベクター型を使うと。
- 文字列
文字列も既存メジャー言語たちとだいぶ違う感じなんだなぁ。
let message = "hello, world"; // 文字列スライスなので &str だからデータ長固定で結合したりできない
let message = String::from("hello, world"); // 可変長文字列として結合したりできる
2.2 関数の実装
関数
Rust だと関数の戻り値なしは空のタプル ()
という特殊な値であり、構造体の一種である。
- 「式」と「文」
お!RustもKotlinみたいに if が式で良き。
あと、;
付けると文、;
無しだと式になるけど、
関数の最後に return する時に式で書くみたいなのは慣れてないとぱっと見わかりづらそう。
(´-`).。oO(サンプルコードがページ跨ぎになってちょっと見づらいのがちょこちょこあるな、あとシンタックスハイライトされてるともっと分かりやすくなりそうだなぁ、電子書籍になったら修正されてると嬉しいなぁ)
2つのエラー
-
?
演算子- 実行結果が
Result
の Error だった場合に、即時 return とかしたい時に使うやつ。await
と組み合わせてよく使いそう。
- 実行結果が
-
unwrap/expect
誤字
- また、特定にメッセージを出力してから
- また、特定のメッセージを出力してから
コメント
ドキュメンテーションコメント(///
)にMarkdown使える、実装例を書いておくと cargo test
でテストとして実行してくれるとか便利ね。
VS Codeでサンプルとか実装する時にフォーマッターとかリンターをコード保存時に適用したくなってきたので、
以下を参考に 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
2.3 制御構造
パターンマッチ
っぱパターンマッチが柔軟だと条件分岐が綺麗に書けていいなぁ
2.4 所有権による安全性
ここらへんRustって感じする(小学生並の感想
所有権
変数を別の変数に束縛した場合、元となる変数からポインタ(メモリのアドレス)
とデータ長
のみコピーされ所有権はが移る。
ただし、Copy
トレイトが実装されている型は値がコピーされるので、
別の変数に渡した後も使える。
fn main() {
let a = 100;
let _b = a;
println!("{}", a); // 100
}
ライフタイム
ライフタイム注釈(&'static str
みたいに使う)でコンパイラに変数のスコープを明示できる。
以下の例は文字列スライスで宣言してるのでプログラムにハードコードされ、
static
ライフタイムを指定してるのでプログラム実行中は有効になる。
fn hello() -> &'static str {
"hello"
}
あれ?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`とか)はまだ無理
}
2.5 データ構造
Java育ちだからクラス無いと不思議な感じ。
構造体
構造体にそのまま変数渡した場合って束縛はどうなるんだろうと思って試したら、
Copy
トレイトありなしの話と同じだった。
メソッド、関連関数
- 構造体にメソッドなどを生やす場合は
impl 構造体名
で定義する - メソッドは構造体のインスタンスを前提としてるため、かならず第一引数に
&self
を受け取る関数となる - インスタンスに紐づかない(第一引数として
&sefl
を受け取らない)関数として 関連関数 を定義することができ、Rustの慣例として インスタンスを生成するnew
関連関数を生やすことが多い。
2.6 async/await
TypeScriptとそんなに変わらなかったのでスッと入ってきた。
- async
誤字
- Futrureトレイトを返す糖衣構文です。
- Futureトレイトを返す糖衣構文です。
RustではJavaScriptでいうPromiseをFuture
トレイトで返す。
- await
JavaScriptと違ってプロパティアクセスのような記法になってる。
?
オペレータとの相性を考慮してそう。
- tokio
Rustで人気の非同期処理ランタイムライブラリ。名前からして農業に強そう。
Rustは歴史的な背景(調べてない)から非同期ランタイムを標準で持ってない。
2.7 クレートとモジュール
最近 TypeScript ばっか書いてるから、ここらへんの公開制御が細やかにできるのうらやまC
- パッケージ
機能を提供する単位。必ず1つ以上のクレートをもつ。
ルートディレクトリのCargo.toml
でパッケージの設定や依存パッケージを定義する。
- クレート
コンパイル単位。ライブラリクレートとバイナリクレートの2種類がある。
ライブラリクレート:他のパッケージに公開するライブラリ週で、公開モジュールはsrc/lib.rs
にまとめる必要あり。
バイナリクレート:src/main.rs
を起点に実行可能なバイナリを生成して main
関数を実行する。複数定義する場合はsrc/bin
は以下のファイルがそれぞれの起点となる。
- モジュール
コードをグループ化し、ファイル分割する。
pub
でモジュール・関数を 1つ上のスコープに公開 できる。pub
なしは同一スコープのみ利用可。
テスト
デフォルト(Cargo)でテスト環境提供されてるの良き。
単体テスト
単体テストのコード実行しようとしたけど、main.rs
と別ファイルにしたら対象外になって実行されなかった。
rust-analyzer
の指摘通りに テスト.rs のテスト対象関数を pub にして、main.rs
に mod テストファイル名;
を追加したらテスト対象として実行された。
-
テストコードはテスト対象のモジュールと同じファイルに実装する
- これテストパターン多くなってきたり、テスト共通の関数とか生えまくってきた時に見づらくなったりしないのかな?
-
cfg(test) 属性
- 環境に応じてコンパイルを切り替える属性風マクロ。テスト時のみコンパイルなどを定義できる。
単体テストのコードはまとめて以下のようにしておくと便利#[cfg(test)] mod tests { // ... }
- 環境に応じてコンパイルを切り替える属性風マクロ。テスト時のみコンパイルなどを定義できる。
-
test 属性
- テストケースとして認識させるためにテストメソッドに設定する。
#[test] fn test_case() {
- JUnit の
@Test
アノテーションみたいなもんやね - あとメソッド名に日本語使えた。
fn add_1足す1は2()
みたいな感じで。
- JUnit の
- 引数無し、戻り値は空タプルか
Result
のみ許容。Reuslt
返せるので?
オペレータも使える
- テストケースとして認識させるためにテストメソッドに設定する。
-
アサーション
-
assert!
マクロ:引数がtrue
か -
assert_eq!
マクロ:2つの引数が等価であるか。引数の型はPartialEq
を実装している必要あり
-
ドキュメントテスト
これは一番使われそうなパターンをドキュメントテストとして書いて、
他の細かいパターンがあれば↑の単体テストを別で書いてくスタイルが良さそう。
ただサンプルコードの構成的にどこにどのファイル置いてるのかピンとこなくて、
最初に作ったhello_world
のmain.rs
と同じ階層にファイル配置してみてDocument Test実行してもno library targets found in package 'hello_world'
になっちゃう。。。
結合テスト
ライブラリクレートで外部公開する関数をテストする際に、srcと同階層に tests ディレクトリ作成して外部呼び出しテストを実施できる。
2.9 よく使うライブラリ
Err をより扱いやすくする anythrow/thiserror
エラーハンドリングを簡易にするたために活用できる
JSON を扱う Serde
Rust でJSON(YAML/TOMLなどもOK) を使う場合のシリアライズ/デシリアライズライブラリ。
Webフレームワークでよく使われている。
3.1 axum とは
axum とは
2022年現在、以下のような選択肢があるが、tokio
との親和性やマクロを使わない設計などに着目し、API実装にはaxum
を使う。
- 古参
- 新参
[参考]insomnia のすすめ
Postman くらいしか使ったことなかったけど使ってみる。brew install insomnia
でもイケる。
あとちらっとPostmanとの比較記事とか無いかなぁと調べた感じ、
MacならPostman, insomniaではなく Paw(初見)もオヌヌメだよって話もあった。
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());
3.3 テスト
テスト実装するとAPI実装してる感爆上げ。
あと、#[cfg(test)]
みたいに環境によってコンパイルされなくなるようなやつ良いよね。TypeScriptにも欲しい。
余談だけど、VS Codeで実装中の時に、コード保存時だけでなく常にLintしてくれるオプションは無いのだろうか😑
3.4 Todo 情報を保存する
なんかカタカナ以外の表記で、1文字ごとにスペースが入ってる表記がちょこちょこあるの何故だろう?フォントのせいかな?
TodoRespository を仮実装
-
RepositoryError
のNotFound
に付与してる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());
に変えたら解消された。
3.5 HTTP リクエスト
SpringのControllerのアノテーションみたいに、それぞれのファイルでルーティングやHTTPステータスコード決められるやつもあるのかなぁ。
ハンドラー
main.rs
の get(all_todo::<T>)
のところで以下のエラーが表示された。
単純に handlers.rs
の all_todo
の戻り値型に impl
つけ忘れだった。
create_app
のテスト実装で、以下の部分でWarning出たので、指摘通りunwrap_or_else(|_| panic!("cannot convert Todo instance. body: {}", body));
にしたら解消された。他の expect も同様。
あと build_todo_req_with_json
とbuild_todo_req_with_empty
の引数の順番で、
pathとmethodが逆になってるのなんか理由あるのかな?と思い合わせてみたけど特に問題なさそう。
DELETE: /todos/:id Todo 情報の削除
時々出てくる .map(|_|, xx)
の第一引数は TypeScript の Array.map
の第一引数みたいなもんかな?
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
アノテーションみたいな。