整理対象
手書きパーサ、段階的なデータ変換、状態機械、proc macro は一見別分野に見えるが、実はどれも「入力の曖昧さを減らし、型に落とす」処理を書く目的で繋がっている。この記事では、Rust が入力ルールをどう型に閉じ込めるかを整理。
Token と Expr の分離
Rustではトークン列、AST、評価器、エラー型を全て型として分離できる。
pub enum Token {
Number(i64),
Plus,
Minus,
Star,
Slash,
LParen,
RParen,
}
pub enum Expr {
Number(i64),
Unary {
op: UnaryOp,
expr: Box<Expr>,
},
Binary {
left: Box<Expr>,
op: BinaryOp,
right: Box<Expr>,
},
}
この切り方の利点は、字句解析と構文解析の責務が混ざらないこと。lex は「どの記号が来たか」だけを扱い、Parser は「どの結合順で木を作るか」だけに集中する。責務ごとに enum を分けたほうが後で読みやすい。
優先順位は関数階層で表現
この parser は再帰下降構文解析。parse_primary、parse_unary、parse_factor、parse_term と関数を積み上げる。重要なのは、演算子優先順位を if 文ではなく関数境界で表す点である。
読み順が、そのまま処理の優先順位の説明になる。
parse_primary: 数字と括弧parse_unary: 単項-parse_factor:*と/parse_term:+と-
この構成では、パーサは「トリッキーな再帰」ではなく、「強く結びつく構文ほど下の関数に押し込む」構造として読める。実装テクニックではなく設計方針として重要。
Parse と Convert の分離
業務データ寄りの例でも、処理を ingest、normalize、parse、validate、convert に分解。
特に重要なのは、parse と convert の分離。
parse: 行を列へ分解するvalidate: 禁止条件を弾くconvert: 文字列を型付き値へ落とす
例えば ParsedFields はまだ文字列を保持する。
struct ParsedFields {
name: String,
date_text: String,
amount_text: String,
}
この段階で確定しているのは、「三つの列に分けられた」という事実だけである。その後 LoadedRecord へ変換されて、初めて「使えるデータ」になる。parse は「意味づけ」ではなく「構造の回収」。
状態集合は enum で明示
struct で状態を持つ例と enum で状態を持つ例を比較。後者では、あり得る状態がコード上に明示される。
enum State {
Accumulating { acc: i32 },
Empty,
}
enum Event {
Add { x: i32, y: i32 },
Reset,
}
これを step(state, event) で遷移させると、表現したい状態集合が明確になる。struct State { acc: i32 } でも動くが、Empty のような特別状態を入れたいとき、無効値や追加フラグを持ち込みやすい。
macro は構文の入口
macro 周りの小さな実験も、パーサや状態機械と同じ線で読める。
use proc_macro::TokenStream;
#[proc_macro]
pub fn double(input: TokenStream) -> TokenStream {
let expr = syn::parse_macro_input!(input as syn::Expr);
quote::quote! {
(#expr) * 2
}
.into()
}
macro もまた、「入力を受け取り、構造として読み直し、別の形に出力する」仕組み。対象が runtime の文字列ではなく compile time の token tree になっているだけで、処理の性質は parser に近い。
特に parse_macro_input! を使うと、不正入力を panic ではなくコンパイルエラーとして返せる。
設計方針としての整理
ここまでをまとめると、各実験の役割は次の通り。
| 実験 | 主な役割 | 型設計の学び |
|---|---|---|
| 手書きパーサ | 字句解析と AST 構築 | 入力規則を enum と再帰構造に分離する |
| 段階的なデータ変換 | 業務データの段階的変換 | parse と convert を分けてエラーの意味を守る |
| 状態機械 | 状態遷移の表現 | あり得る状態を enum に持たせる |
| 式や block の観察 | Rust 構文の観察 | Rust 構文そのものを入力として意識する |
| proc macro | token からコード生成 | compile time の parser として macro を見る |
対象は違っても基本姿勢は同じ。曖昧な文字列やイベント列を曖昧なままロジック側に流さず、段階ごとに別の型へ変えていくこと。
設計の成功と失敗
以前は、型を増やすと実装が重くなると考えていた。しかし、「どの段階で何が確定しているか」の曖昧なままにすることこそが実際に重くなる原因。型の数が増やすことは、コード量こそ増えるが、その曖昧さを解消する利点が得られる。
たとえば次の設計は、短くても後から読むと危険。
Stringのまま日付や金額を持ち続ける- パース失敗と業務ルール違反を同じ
Err(String)に押し込む - 状態機械の特別状態を
boolや sentinel 値で表す
反対に、段階ごとに名前のある型を置くと、コード量は少し増えても論点は減る。「型を増やす勇気」は、実装トリックより価値がある。
次にやるなら
この系列の次の実験候補。
- 式パーサに識別子と
let文を追加する ParseErrorに line/column を持たせて診断を改善する- 状態機械の例を generic event source に広げて、遷移表を外出しする
- proc macro で attribute macro も試し、宣言的 macro との差分を比べる
要点は、Rust の型設計は「複雑さを消す」より「複雑さの位置をずらす」作業に近いということ。入力の曖昧さを型の境界で止めることが、読みやすさと保守性につながる。