前置き
これは- 多人数の開発で
- Rustに不慣れな人間もかなり混ざってて
- 富豪的プログラミングが可能な Web 開発で
- コードオーナー不在で
- Rust 1.0 公開直後の error_chain や tokio_core の時代から今までコードを保守してきた
経験から得た 恨み節 生存戦略 知見である
- ↑の前提がなければツッコミどころ満載なのは自覚している
- この話は表だって書くと社内の人間や会社の名誉を傷つけかねない
ので gist に放流した
- ノートパソコンのCPUとメモリでは限界がある
- cpu 二桁コアのマシンを何人かで共有して使え
- VSCode の Remote SSH でがんばれ
- neovim でもいいぞ
- target はブラックホール
- 10GB 超はあたりまえ、中には 100GB 超も
sccache
、cargo cache
、cargo sweep
を組み合わせて自衛しろ-
$ cat ~/.cargo/config.toml [build] rustc-wrapper = "/home/legokichi/.cargo/bin/sccache"
- docker も使うので大容量ストレージだけが正義だ
- 一番時間とメモリを食うのはシングルスレッドしか使わない ld でのリンク
- 並列リンクできる mold は絶対使え
-
$ cat ~/.cargo/config.toml [target.x86_64-unknown-linux-gnu] linker = "/usr/bin/clang" rustflags = ["-Clink-arg=-fuse-ld=/usr/local/bin/mold"]
- ビルドでメモリを使うのはリンク時のみ
- 実はメモリは16GBもあれば十分かもしれない
cargo build –timings
で計測しろ- 謎の features のせいでビルド時間が律速してることが多々ある
- 計測しろ
- 開発メンバーの開発環境はてんでバラバラだ
- windows10,11, wsl1,2
- mac x64, m1, m2
- ubuntu busuter,bullseye,bookworm
- debian, archlinux, NixOS, asahilinux, amazonlinux1,2
- iPad, android, chromebook
- くらい統一されてない
- docker しか信用できない
- docker のバージョンも厳密に統一しろ
- docker engine バージョン違いで挙動は変わる
- 常識
- 使いたい機能があっても nightly は論外
-
[toolchain] channel = "1.72.0" components = [ "rustfmt", "clippy" ] profile = "minimal"
- 常識
- オレオレフォーマットを主張するやつは相手にするな
- パラノイアになれ
- 長いものにまかれろ
- 大抵のことは docs.rs を読めばわかる
- でも何をやってるのかわからなくなるので docs.rs にある source リンクを押してソースを読むことになる
- でも何が起きてるのかわからなくて
cargo new hogecrate-sandbox
で playground を作って試すことになる - でも実は docs.rs に書いてあったりする
- rust において信じられるのは major version ではない、作者への信用だけだ
- 1.x になっても非互換な変更を入れてくるやつはいる、aws-sdk-rust とか
- 信用できるのは serde とか tokio とか、そのへんの作者
use hoge::A as HogeA
とかするな- 愚直に
hoge::A
とタイプしろ - お前は読めても他の人間は読めない
type Result<T> = Result<T, MyError>
みたいな std の型名を上書きするのは論外- お前のことやぞ
use anyhow::Result;
use std::error::Error;
とuse std::io::Error;
を見分けられる人間だけが石を投げなさいuse std::time::Duration;
とuse chrono::Duration;
use thiserror::Error:
とuse anyhow::Error:
- 等々
- お前は読めても他の人間は読めない
- trait alias も同様
- お前は読めても他の人間は読めない
- 部分コピペで動かなくなるコードは作るな
use std::sync::mpsc::channel
とかするなuse hoge::Error
とかするなstd::error::Error
と区別がつかなくなるから- channel とかの一般名刺が突然出てきてもわからなくなる
- お前は読めても他の人間は読めない
- 部分コピペで動かなくなるコードは作るな
std::rc::Rc<std::cell::RefCell<T>>
とかBox<dyn std::future::Future<Output=T>+ Send + Sync + 'static>
とかのタイピング練習を欠かすな- でも
use std::rc::Rc
とかstd::sync::Arc
とかならゆるしちゃうかも - でも
use tokio::sync::Mutex
とかが突然生えてきたりするので自衛のためにstd::sync::Mutex<T>
と書いてしまう - 関数内の先頭なら許す(コピペ可植性が高いので)
- でもモジュールの先頭とかには書かないでくれ
- コードを書くときは楽ができるかもしれないが、コードを保守する側としては大変困る
- git で pull request を作ってもコンフリクトの主要な発生源になり大変
use std::{thread::sleep, *}
みたいな書き方は言語道断である- Smithay/smithay のこのコードを見て発狂しないやつだけが石を投げなさい
- ファイル先頭に限らず prelude は使うべきではない
- 何が import されるのか、お前以外は予想できない
- お前は読めても他の人間は読めない
- 脳死で
Arc<Mutex<T>>
してClone + Send + Sync + 'static
しろ - 業務で生体参照ライフタイムソルバするのは不毛
- メモリ効率とか速度とか気にするな、顧客へのデプロイ速度がすべてだ
- trait でオレオレ DSL を作ろうとするな
- お前は読めても他の人間は読めない
- rust-analyzer で定義に飛んで trait だったときの絶望感を味わえ
- 誰にも読めない完璧な抽象化コードよりも、誰でも読めてどこにでもコピペできる愚直で平易なコードが長く生き残るコードだ
- お前にしか読めないコードよりも誰でも読めるコピペコードのほうがマシだ
- 修正箇所が n 箇所のコピペで済むならコピペのほうがマシだ
- 誰にも読めない完璧な抽象化コードよりも、誰でも読めてどこにでもコピペできる愚直で平易なコードが長く生き残るコードだ
- 小規模コードならともかく、コードが大きくなるにつれて動かなくなる
features
を認識しなかったりバージョン違いなどで、いずれ動かなくなる- 複数回のコード定義ジャンプしないと理解できないようなコードを書くな
- 「n回のコピペ(切り貼り)で移植可能なコード」のnが小さいほど可読性、可植性が高い
- 誰でも読めてどこにでもコピペできる愚直で平易なコードが長く生き残るコードだ
cargo clippy --tests --examples 2>&1 | head -n 40
だけを信じろ- 下の方のエラーは上のエラーが引き起こしているので読むだけ無駄である
- どうせマルチスレッド、マルチ非同期タスクのコードのデバッグにはなんの役にもたたない
println
とlog::debug
だけが唯一の信頼できるデバッグ情報だ- テストを書いて print debug で二分探索するのが最も早いデバッグ手段だ
- print すると再現しないバグは存在する
- printlnはスレッド間で同期を取る
- io を伴うテストにはすべからく再現性がない(flaky である)
- aws-sdk-rust すら実行時の挙動に破壊的変更が入る
- aws lambda を aws_lambda_runtime で実行する場合 amazon linux で実行することになるが、 glibc のバージョン違いとかでローカルテストが通っても lambda の中では実行時リンクエラーで動作しなかったりする
- e2e test だけが唯一信用できる
- RealWorld 業務 Rust では libssl や libsqlite3 などの共有ライブラリに頼ることになるので musl は使えないと思え
- ↑2つは bundled があるのでフルビルドでなんとかなるが RealWorld では他にも共有ライブラリに頼ることになるのでいずれ musl は使えなくなる
- cargo-zigbuild なら glibc のバージョンが固定できるというは幻想で、できる場合もある、のが正しい
- パッチバージョンを上げただけでバグる crate は存在する
- aws-sdk-rust とか
huga()?
(the question mark operator) をそのまま使うな
- エラーを見てもどこで何がおきたかわからん
use anyhow::Context;
してhoge.huga(param).context(format!("huga {param:?} で落ちた"))?
を書きまくれ- backtrace も有効化しろ
- release にも debug 情報は残せ
-
しろ
[profile.release] debug = 1
- error-chain も failure も thiserror も信用できない
- 結局信じられるのは stack trace の関数名と行番号だけだ
- backtrace が有効なら
.expect("ここで落ちた")
を頑張る必要はない - 安心して
.unwrap()
してくれ
- 一番使われている crate がいちばんいい crate だ
- 謎の crate を自作するな公開するな
- Rust の Builder Pattern は crate で API を公開するときのメジャーバージョンの互換性のために使われている
- 非公開内製 crate なら Builder Pattern で setter を生やすよりも Paramater struct を引数にとる new method だけで十分
- お前のことやぞ aws-sdk-rust
- rusoto は良かった、本当に…
- init pattern が好き
- log を使っておけばテストやデバッグでも潰しが効く
- tracing にも対応できるぞ
- とりあえず main 関数には脳死で env_logger 入れとけ
- テストにも脳死で
env_logger::builder().is_test(true).try_init().ok()
って書いとけ - これが業務 rust の "おまじない" だ
- Rustのエラー処理はResultのネストが正解
- エラーには分類(回復)可能なものとそうでないもの(panic相当)がある
- 回復不能なものを別にanyhowとしてくくりだすことで
?
を使いつつ柔軟なエラー処理が書ける -
#[tokio::main] async fn main() -> Result<(), anyhow::Error>{ let o = match foo().await? { Ok(o) => o, Err(CustomError::A) => { todo!() } _ => { todo!() } } }
- 単にpanicさせるのではなくエラーレポートを書きたいなどのときに、パニックハンドラのようなlow-levelの処理に頼らなくても良くなるので便利
_
で束縛した変数は実は束縛されず、その場で drop される- これは実は変数束縛ではなくパターンマッチング
- 変数スコープを抜けるときに drop されれる他の変数とは処理が異なる
- ややこしいから unused variable warning を避ける目的なら
_hoge
のように名前をつけろ - 公式ドキュメントにも RFCS にも "明示的には" 載ってない挙動です
- Ignoring an Entire Value with _
- wildcard-pattern
- [Rust] _(underscore) Does Not Bind
- Rustにおけるirrefutable patternを使ったイディオム
- destructors
- ややこしい
serde-json
かserde_json
か間違えたことがないものだけが石を投げなさい
async fn
の返り値の future が持つ参照のライフタイムは記述できない- 例: https://github.com/launchbadge/sqlx/pull/1687/files#diff-9a071fcf9db3c54bc8e2179258161fccd6d7fc8cf3d2c6ae390213c2e87b9bf8R38
- clippy が
warning: this function can be simplified using the async fn syntax
とか言ってくるが#[allow(clippy::manual_async_fn)]
で黙らせろ
#[allow(clippy::manual_async_fn)]
fn run_query<'a, 'c, A>(conn: A) -> impl Future<Output = Result<(), BoxDynError>> + Send + 'a
where
A: Acquire<'c, Database = Postgres> + Send + 'a,
{
非同期rustを書いているとStringのcloneが頻発する(&strがライフタイム的にできない)がちゃんとArcに包んでますか?