Open8
読書メモ『Effective Rust』

このスクラップについて
『Effective Rust』の読書メモ。本の内容をまとめたり再構成したり、感想をまとめたりする。

前書き
- この本には「~してはいけない」という項目がない
- なぜなら本当にしてはいけないならコンパイラがエラーにするべきだから
- これは非常に重要な視点
- 現実的にはRustにはそういう面(そう書いちゃいけない)はあるだろうけど、かなり少ないはず
- 人間はそういう言語を使うべき
- プラクティスで回避しないといけないものは、実際の現場では回避されない
- みんながみんなプログラミングが好きじゃない、勉強する時間もない
- 常にコスパを求められ時間に追われる労働者は、長期的な視点に立った学習への投資はされにくい
- だからコンパイラ、言語仕様でいい書き方を強制すべきである
- そしてそれは実行前に行われるべきである
- フィードバックループを高速に回すため
- なので表現力があり厳格な型システムおよびその検査器がいる
- そしてそれは実行前に行われるべきである
- プラクティスで回避しないといけないものは、実際の現場では回避されない

1章 型
項目1: データ構造を表現するために型システムを用いよう
- enum型を特に推している
- enum型は自分でデータの型を定義できる
- とり得る値の全てを列挙することでMECEによる分析ができる、それを使った場合分けなどで認知負荷が減る
- match式によるパターンマッチではコンパイラが網羅性検査をする
- 代数的データ型があるなら当然その機能も欲しい
- match式によるパターンマッチではコンパイラが網羅性検査をする
- 明示的に名前をつけることで他の型を区別することができ、間違って別の値を入れることができなくなる
- コンパイルエラーになるため
- newtypeパターンと一部効能が被る
- newtypeパターンは既存の型でラップするので、構造が同じ(例えば2値しかないならboolをラップするなど)既存の型がある場合はnewtypeパターンを使えばいい
- とり得る値の全てを列挙することでMECEによる分析ができる、それを使った場合分けなどで認知負荷が減る
- enum型は自分でデータの型を定義できる
- 代数的データ型には不変条件を埋め込むことができる
- 例えば、Rustでは上の例を下の例にすることでプログラムの普遍条件をプログラムの中にエンコードすることができる
- 上の例は、
DisplayProps
として不適なものまでDisplayProps
型の値として扱うことができてしまう- プログラマは、
DisplayProps
という型の値が来た時に、常にそれが valid なものなのかどうかについて認知を割かないといけない- テストもその分大変だろう
- 人が書いたコードならなおさら
- テストもその分大変だろう
- プログラマは、
- 下の例は、代数的データ型を活用することによって、不適な値はそもそも作れないようになっている
- プログラマは、不適な値がまざっていることを一切考えなくて良くなる
- 上の例は、
- 例えば、Rustでは上の例を下の例にすることでプログラムの普遍条件をプログラムの中にエンコードすることができる
pub struct DisplayProps {
pub x: u32,
pub y: u32,
pub monochrome: bool,
// `fg_color` must be (0, 0, 0) if `monochrome` is true. `monochrome`が真なら`fg_color`は(0, 0, 0)でなければならない
pub fg_color: RgbColor,
}
# x, y は共通してもっていて、モノクロかそうでないかで構造が変わる
pub struct DisplayProps {
pub x: u32,
pub y: u32,
pub color: Color,
}
# モノクロかそうでないかという排他的な状態は enum で表現できる
pub enum Color {
Monochrome,
Foreground(RgbColor),
}
struct RgbColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
-
Option<T>
やResult<T, E>
など、Rustのコアの型ではこのenumの恩恵をふんだんに受けている
項目2: 型システムを用いて共通の挙動を表現しよう
- マーカートレイトは、静的検査がほぼ効かないトレイト
-
StableSort
というトレイトで安定ソートの振る舞いを定義したいとする- それが安定ソートであるということを型システムで検証するのはかなり高度でより複雑な型システムがいる
- 型システムによる検証はあきらめけるけれど、モジュールの提供者と利用者間の約束事としてそれを実装することにした
- 提供者はこれが安定ソートでもあるということをマーカートレイト(ここでは
StableSort
)を実装することで宣言する- この正しさをコンパイラが検証することはできない
- 利用者は、提供者のその宣言を信じて
StableSort
を実装しているトレイトを利用する
- 提供者はこれが安定ソートでもあるということをマーカートレイト(ここでは
-
項目3: OptionとResultに対してはmatchを用いずに変換しよう
-
Option
、Result
間(型パラメータが違うもの同士も含めて)の変換は大体メソッドが提供されているので探してみるといい -
as_ref()
メソッドのように、&Container<T>
とContainer<&T>
を行き来する処理も典型なのでメソッドがすでに提供されている可能性が高い、使いこなそう
項目4: 標準のError型を使おう
- DebugとDisplayを実装すればErrorになれる
-
?
は対象のエラー型(Result<T,E>
のE
)に自動で変換を試みるので、From
トレイトを実装しておくとなお良い -
anyhow
やthiserror
がなぜ便利なのか、どんな問題を解決してるのか - アプリケーションなら
anyhow
でいい- 利用される環境が予測できないライブラリでは
enum
を用いてネストしたエラーを定義した方がいい- 情報をたくさんもちたいため
- 利用される環境が予測できないライブラリでは
項目5: 型変換を理解しよう
- 3種類ある
-
From
、Into
トレイトによるもの -
as
を用いた明示的なキャスト - 暗黙に行われる自動型変換(coercion)
-
-
From
を実装していれば自動的にInto
も導出されるようになっているが、逆はない -
as
ではなくできるだけfrom
/into
をつかいたい- 前者は後者ではできない型変換も許容するが、これはよくないと筆者は判断している
- Clippyに強制されるのもあり
-
as
による明示キャストは自動型変換の真のスーパーセットになっている- 自動型変換
- 可変参照から不変参照
- 参照から生ポインタ
- 変数をキャプチャしていないクロージャから関数ポインタ
- 配列からスライス
- ある型からその型が実装しているトレイトのトレイトオブジェクト
- 生存期間が長いものから短いもの
- 自動型変換
項目6: newtypeパターンを活用しよう
- 型エイリアスはただのドキュメンテーションでしかなく、コンパイラに区別させられない
-
SomeNewType(T)
はほとんどT
であるにもかかわらず、T
が実装しているトレイトを手動でSomeNewType(T)
にも実装し直さないといけない
項目7: 複雑な型にはビルダを使おう
- 想定する(ビルドしたい型)は、
Option<T>
じゃない必須のフィールドと、Option<T>
の必須じゃないフィールドがあるやつ-
HogeBuilder(T)
という別の型を用意する -
new
メソッドでは必須のフィールドをもらう - あとは
mut self
をもらってself
を返すメソッドでオプションフィールドを追加する - 最後に呼ぶ用の
build(self) -> T
メソッドを用意する
-
項目8: 参照型とポインタ型に慣れよう
- 参照は最もよく使われるポインタ
- 安全に使うための制約がいくつもある
-
Deref
は一つの実装につき1つの参照解決先(Target
)をもたないといけない(自動でDeref
するのでその変換先が一意に定まらないといけない)-
AsRef
、AsMut
は自動変換がないのでパラメタ化できる
-
- ファットポインタ
- スライスとトレイトオブジェクト
-
Borrow
トレイト- 借用を一般化
- &self -> T の変換をI/Fとして提供する
-
ToOwned
トレイト-
Clone
の一般化
-
項目9: 明示的なループの代わりにイテレータ変換を使用することを検討しよう
- 明示的なループの方がいい場合もある
- ループボディが大きい場合など
- ただこのばあいはいくつかの宣言的なI/Fに切り出した方がいいかも
- ループボディが大きい場合など

2章 トレイト
項目10: 標準トレイトに習熟しよう
-
Copy
トレイトはマーカートレイト- メモリのビット単位のコピーで新しいアイテムが作れることを意味する
- 代入の操作的な意味を根本的に変える
- 実装しない方がいい/してはいけない場合の例
- 型が大きい場合
- copyは高速に行われることを暗黙に期待されている
- 型が大きい場合
項目11: RAIIパターンにはDropトレイトを実装しよう
- Resource Acquisition Is Initializationという名前から初期化時に意識がいきそうだけど、ミスリードだと思う
- ある値の生存期間と、それに紐づくデータ、資源の生存期間を一致させるというのが本質
- つまりある値がスコープから外れる時に、それに紐づく資源も一緒に解放するようにしよう
- ロックとか最たる例
- だから
Drop
トレイトが必要になってる来るんですね - 他の例
- OSの資源にアクセスするデータ
- ファイルディスクリプタdc
- OSの資源にアクセスするデータ
項目12: ジェネリクスとトレイトオブジェクトのトレードオフを理解しよう
項目13: デフォルト実装を用いて、実装しなければならないトレイトメソッドを最小限にしよう

3章 さまざまなコンセプト
項目14: 生存期間を理解しよう
- プログラミング言語理論ではスコープには馴染みが深いので、概念自体は全然抵抗がない
- 自分はこのライフタイムありの体系を研究していたこともある(修士)
- ヒープとスタック
- スタック
- 現在実行中の関数に関する状態を保持するため
- 関数の引数
- 関数内のローカル変数
- 関数内で一時的に計算された値
- 関数の呼び出し元コード内のリターンすべきアドレス
- スタックの値は短命で、こいつがもういないのにポインタは残ってるみたいなのがよくおこる問題
- C言語はローカル変数のポインタを返してしまうことは普通にできる
- このスタックとヒープの部分は抽象化の難しいところ
- 微妙に隠してるんだけど、ちゃんと使うためにはしらないといけない
- 現在実行中の関数に関する状態を保持するため
- スタック
- 一時変数のスコープの話で出てきた以下の例
-
return_ref
への引数を& Item
にするとコンパイルできるのなんでだろう-
mut
かどうかに関わらず一時変数はつくられるよね - 原則通りだとどっちもアウトになるはずだけど、
mut
つけない方はコンパイラがアドホックにライフタイムの延長をしていそう
-
-
struct Item {
content: u32,
}
fn return_ref(item: &Item) -> &Item {
item
}
fn main() {
let r: &Item = return_ref(&mut Item { content: 42 });
println!("Item content: {}", r.content);
}
-
'static
ライフタイム- プログラム実行中は常に同じアドレスに存在し、移動しないことを保証する
-
const
も'static
ライフタイムに昇格できるが、注意点が2つ-
Drop
を実装しているとできない - 常に変わらないことが保証されるのは「値」だけ
- コンパイラは
const
値を使う場所ごとにコピーを作れる- 複数のアドレスにコピーが作られる
-
-
ヒープ上に確保され、解放されない値も
'static
の定義を満たす-
Box<T>
は違う- ドロップされないことを保証できないから
- ただし
Box::leak
を使うとできる- ああ、型も
hoge -> &'static mut T
なんだろうね -
Box<T>
をT
への可変参照に変換するもの - 誰も所有者がいなくなるので解放もされない
- ああ、型も
-
-
ヒープに関して
- 全てのアイテムには所有者がいるというのがポイント
- その所有者のライフタイムと一致する
- 所有者がヒープにいる場合もある
- 連鎖を辿っていけばスタックに帰着される
- 全てのアイテムには所有者がいるというのがポイント
-
よくでてくる
<'_>
について- これはユーザーの定義した型が、ライフタイムに関連づけられていることを示す手軽な手段として使われている
- Rustのシグネチャにはライフタイムを省略していい場合があるけど、ユーザー定義型からそれを外すとライフタイムの必要な参照を持っていることがわかりにくくなる
項目15: 借用チェッカを理解しよう
- 参照の生存期間にはノンレキシカル生存期間機能がある
- その変数が定義されたブロックじゃなくて、最後に使用した時点までに縮められる
- 相互接続されたデータ構造にはスマートポインタを積極的に使おう
-
Rc
はRefCell
と組み合わせることが多い
RefCell
は可変参照なしで内部状態を変更できるが、借用チェックを実行時に後退させてしまう -
Mutex
はマルチスレッド環境での内部可変性を実現する
-
項目16: unsafeコードを書かないようにしよう
- unsafeが必要な場合、それを実現している権威のあるクレートを使うのがいい
- こういうスタンスだからか、unsafeについては全然説明してない
項目17: 状態共有並列実行には気を付けよう
項目18: Don't panic
項目19: リフレクションを避けよう
- そもそもできないので守るのは簡単
- 同じ依存ライブラリの互換性のない複数バージョンを1つのプログラムをロードしたい場合に、1つのライブラリは1つだけしかリンクできないという制約を回避するためにリフレクションを用いる場合がある
- RustはCargoが同じライブラリの複数バージョンを扱えるので必要ない
項目20: 過剰な最適化の誘惑を退けよう
- 最適化は必要になったら使えばいい
-
.clone()
とかはコストを明示できるのがえらい - スマートポインタがんがんつかっていけ

4章 依存ライブラリ
項目21: セマンティックバージョン(SemVer)を理解しよう
- Cargoでは
1.2.3
と指定した場合、1.3.0
も受け入れられる
項目22: 可視範囲を最小化しよう
項目23: ワイルドカードインポートを避けよう
項目24: APIに型が登場する依存ライブラリは再エクスポートしよう
- Cargoが、依存グラフの中に、互換性のない複数バージョンの同一クレートを持てることによる発生する問題を回避するプラクティス
- 以下のような依存経路を考える
- src -> libA(ver 0.8)
- src -> libB
- libA(0.7)
- このとき、libBの中でlibAをpub useすることで、
- 「このライブラリ(libB)の中で使うlibA」をsrcから参照できるようになる
- libBのI/FにlibAが露出する場合に有効
- 以下のような依存経路を考える
項目25: 依存グラフを管理しよう
- 依存グラフのチェックをするツール
-
cargo-udeps
- 使ってないものを検知してくれる
-
cargo-deny
- 推移依存関係にあるさまざまな潜在的問題を検出する
- 既知のセキュリティ問題
- 受け入れ不能なライセンスで提供されているライブラリ
- 推移依存関係にあるさまざまな潜在的問題を検出する
-
項目26: 忍び寄るフィーチャに注意しよう
- まず、Rustコンパイラにはフィーチャーオプションによって制御できる条件付きコンパイルがある
- Cargoのフィーチャー機能は、これをラップしてるだけ
- フィーチャー機能は Additive だというのが罠になりうる

5章 ツール
項目27: パブリックインターフェイスのドキュメントを書こう
- こーどからわかることをドキュメントに書かない
- 特にせっかくRustを使ってるんだから、セマンティクスを型として表現してコードに埋め込むべき
項目28: 分別をもってマクロを使おう
- Rustのマクロは型、識別子、式などのプログラムの断片を抽象化する
- 宣言的マクロと手続きマクロがある
- 手続きマクロ
- マクロの引数以外のソースコードをいじれる
- 柔軟性高いかつコンパイル時に安全性を検証する
- リフレクションに対する制限の緩緩的な側面も持つ
- 3つの分類
- 関数的マクロ
- 引数を用いて呼び出す
- 引数は1つ
- 字句解析したトークン列の直感でいい
- だから自分で構文解析をする必要がある
- 属性マクロ
- プログラムのなんらかの文法要素に付与
- 関数全体を包むために用いられたりする
- deriveマクロ
- データ構造の定義に付与
- 入力トークンを置き換えるのではなく、追加する
- へるぱ属性を宣言することができる
- 関数的マクロ
項目29: Clippyに耳を傾けよう
項目30: ユニットテスト以上のものを書こう
型システムで表現されていることをドキュメントに「書かない」ようにアドバイスした。同様に、型システムで保証されていることをテストする必要はない。enum型の変数に、許可されているヴァリアントのリストにない値が入ることがあったら、ユニットテストに失敗するどころでない大問題だ。
- その通りすぎる
- 表現力の豊かな型システムを使えば普遍条件をエンコードできるのでテストが軽くなるんだよね
- 結合テスト
-
tests/
に置かれる - クレートの公開APIしか使えない
-
項目31: ツールのエコシステムを活用しよう
項目32: CIシステムを設定しよう
- rust-toolchain.tomlを使ってCIビルドで用いるバージョンを固定しよう

6章 標準Rustの向こうへ
項目33: ライブラリコードをno_std互換にすることを検討しよう
- no_std互換にするための、クレートの利用方法などが書いてある
- 他のクレートのソースコードを読んだ時にも遭遇するかもしれない
項目34: FFI境界を通過するものを制御しよう
- Foreign Function Interfaceの名前に反して、連携できるのは関数だけじゃない
- 他言語との相互運用のデフォルトのターゲットはC言語
- FFIコードは自動的に
unsafe
-
extern "C"
- 関数の定義を外部のC言語のライブラリが提供することを示す
- この中の関数は
no_mangle
となる
- Beginner's guide to Linkers
項目35: 手動でFFIマッピングを書かずにbindgenを用いよう
- bindgen
- 基本的な機能は、Cのヘッダファイルをパースして対応するRustの宣言を生成する