📚 背景知識(読んでから問題へ)
昨日(Day 056)は macro_rules! の展開結果を予想しました。要点は3つでした。$e:expr で捕まえた断片は1つの不透明な塊になる(Q3)、メタ変数を2回書けば式が2回評価される(Q4)、関数・型などの項目は衛生的でない(Q6)。今日はこれを使って、実務でそのまま使える4つのマクロを書きます。
今日の中心は「要素数をコンパイル時に数える」ことです。hashmap!{ "a" => 1, "b" => 2 } の中で HashMap::with_capacity(2) を呼べれば、挿入のたびにハッシュ表を作り直す(再確保する)必要がなくなります。数え方には有名な2つの流儀があります。
| 流儀 | 仕組み | 知られている弱点 |
|---|---|---|
| 再帰で数える | 1 + count!(残り) を要素数ぶん展開する | 要素数ぶんマクロの再帰が起きる。再帰の上限 recursion_limit の既定は 128 |
| スライスの長さで数える | 各要素を () に置き換えて [(), (), ()] を作り、<[()]>::len(&...) を取る | 見慣れない書き方。() は大きさ 0 なので配列は実行時に何も確保しない |
どちらも「The Little Book of Rust Macros」の Counting の章に載っている手法です。今日は両方を書いて、どこで壊れるかを測ります。
もう1つ、昨日使わなかったフラグメント指定子を使います。
| 指定子 | 何に一致するか | 今日の用途 |
|---|---|---|
ty | 型(u8、Vec<String>、&'a str など) | 8つの整数型に同じ impl を量産する |
ident | 識別子(UserId など。_ は除く) | 生成する構造体の名前を呼び出し側から受け取る |
vis | 可視性(pub、pub(crate)、空にも一致) | 生成する構造体の公開範囲を呼び出し側に選ばせる |
meta | 属性の中身(derive(Debug, Clone) など) | 呼び出し側が付けた #[derive(...)] をそのまま転記する |
📝 問題
cargo new macro_day057(edition 2024、外部クレートなし)の src/main.rs に、次の4つのマクロを実装してください。すべて cargo test で確かめられるようにテストも書きます。
課題1: count! — 要素数をコンパイル時定数として返す
const N: usize = count!(x, y, z); // 3
let arr = [0u8; N]; // 配列の長さに使える = コンパイル時定数
assert_eq!(count!(), 0);
assert_eq!(count!(a,), 1); // 末尾カンマも可
assert_eq!(count!((1, 2), [3, 4], { 5 }), 3); // 括弧のまとまりは1要素
- 要素が 10,000 個でもコンパイルできること(再帰の上限に当たらないこと)
- 比較用に、再帰で数える
count_rec!も書き、何個で壊れるかを実際にコンパイルして確かめる
課題2: hashmap! — std の vec! と同じ品質で
let m = hashmap! { "apple" => 3, "banana" => 5, "cherry" => 7, };
次の6つの要件をすべて満たすこと。
- 末尾カンマがあってもなくてもよい
HashMap::with_capacity(要素数)で一度だけ確保する(要素数は課題1のcount!で数える)- 各キー・値の式は書いた順に1回ずつ評価される
- 呼び出し側に
mapという変数があっても壊れない #[macro_export]して他のモジュール・クレートから使ったとき、呼び出し側がuse std::collections::HashMap;していなくても動くlet m: HashMap<&str, i32> = hashmap! {};の0要素でも警告が出ない
さらに、ヒープ確保の回数を数えて、with_capacity を付けない版と HashMap::from([(k, v); 100]) の3つを 100 要素で比べてください(#[global_allocator] で数えるアロケータを自作する)。
課題3: impl_checksum! — ty で8型に独立した impl を作る
pub trait Checksum { fn checksum(&self) -> u8; } // リトルエンディアンのバイト列の和(下位 8bit)
impl_checksum!(u8, u16, u32, u64, i8, i16, i32, i64);
assert_eq!(0x0102u16.checksum(), 3);
assert_eq!((-1i32).checksum(), 0xFC);
課題4: newtype! — meta・vis・ident・ty を全部使う
mod ids {
newtype! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(u64);
}
newtype! {
#[derive(Debug)]
struct Secret(String); // vis 無し = このモジュールの中だけで使える
}
}
let id: ids::UserId = 42u64.into(); // From<u64> が生成される
assert_eq!(id, ids::UserId::new(42));
assert_eq!(id.into_inner(), 42);
生成するのは「構造体」「new」「into_inner」「From<内側の型>」の4つ。new と into_inner の可視性は、構造体と同じ $vis にしてください。
設計の設問(コードの後に答える)
- (D1)
count!の2つの流儀のうち、どちらを採るか。測った数値を根拠に答えてください - (D2)
hashmap!と std のHashMap::from([("a", 1), ("b", 2)])のどちらをチームの標準にするか
💡 ヒント
ヒント1(方向性)
- 課題1: 「数を足す」のではなく「同じ個数の何かを並べて、並べたものの長さを聞く」と考えると、再帰が要らなくなります
- 課題2: 要件5は昨日の Step 3(型名は呼び出し側で解決される)、要件3は Q4(何回書いたか)、要件6は「0回の繰り返しで何が残るか」を思い出してください
- 課題4:
visは「何も書かれていない」にも一致します。$vis:vis structと書けばpubの有無の両方を1つの規則で受けられます
ヒント2(アプローチ)
- 課題1:
macro_rules!の中に、外から呼ぶつもりのない内部規則を(@unit $_t:tt) => { () };のように@で始めて置くのが慣習です。各要素を()に変えて[(), (), ()]を作り、<[()]>::len(&[...])を呼びます。<[T]>::lenはconst fnなのでconstの初期化に使えます - 課題1:
hashmap!からcount!へキーを渡すとき、キーはもうexprの塊です。count!側を$x:ttで受ければ、塊は1個のトークン木として一致します(昨日の Q3 の逆向き) - 課題2: 絶対パス
::std::collections::HashMapと、同じクレートのマクロを呼ぶ$crate::count!を使います - 課題2: ヒープ確保の回数は、
std::alloc::Systemに委譲しつつallocとreallocで数えるGlobalAllocを書けば取れます。cargo testはテストを並列スレッドで走らせるので、カウンタの置き場所に注意
ヒント3(コード骨格)
macro_rules! count {
(@unit $_t:tt) => { () };
($($x:tt),* $(,)?) => { <[()]>::len(&[ $( $crate::count!(@unit $x) ),* ]) };
}
#[macro_export]
macro_rules! hashmap {
() => { /* 0要素の規則を先に */ };
($($k:expr => $v:expr),* $(,)?) => {{
let mut map = ::std::collections::HashMap::with_capacity($crate::count!($($k),*));
$( map.insert($k, $v); )*
map
}};
}
macro_rules! newtype {
($(#[$m:meta])* $vis:vis struct $name:ident($inner:ty);) => {
$(#[$m])*
$vis struct $name($inner);
// impl $name { ... } と impl From<$inner> for $name { ... }
};
}
✅ 模範解答
cargo new macro_day057 の src/main.rs をこれで置き換えます。Cargo.toml は生成されたまま(edition = "2024"、依存なし)です。
模範解答のコード全体を開く(自分で書いてから)
use std::alloc::{GlobalAlloc, Layout, System};
use std::collections::HashMap;
use std::cell::Cell;
// ---------- 計測用: ヒープ確保の回数を「スレッドごとに」数えるアロケータ ----------
// cargo test はテストを並列スレッドで走らせるので、グローバルな1個のカウンタだと
// 他のテストの確保まで数えてしまう(実測: 1 のはずが 9)。
struct CountingAlloc;
thread_local! {
static ALLOCS: Cell<usize> = const { Cell::new(0) };
}
fn bump() {
ALLOCS.with(|c| c.set(c.get() + 1));
}
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
bump();
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
bump();
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static GLOBAL: CountingAlloc = CountingAlloc;
fn allocs_during<T>(f: impl FnOnce() -> T) -> (T, usize) {
let before = ALLOCS.with(Cell::get);
let out = f();
(out, ALLOCS.with(Cell::get) - before)
}
// ---------- 課題1: count! — 要素数をコンパイル時の定数として数える ----------
#[macro_export]
macro_rules! count {
// 内部規則: どんなトークン木も () に置き換える
(@unit $_t:tt) => { () };
($($x:tt),* $(,)?) => {
<[()]>::len(&[$($crate::count!(@unit $x)),*])
};
}
// 比較用: 再帰で数える素朴版(要素数ぶん再帰する)
#[macro_export]
macro_rules! count_rec {
() => { 0usize };
($head:tt $(, $rest:tt)* $(,)?) => { 1usize + $crate::count_rec!($($rest),*) };
}
// ---------- 課題2: hashmap! ----------
#[macro_export]
macro_rules! hashmap {
// 0要素の規則を先に置く。無いと下の規則で `let mut map` が一度も変更されず unused_mut 警告
() => { ::std::collections::HashMap::new() };
($($k:expr => $v:expr),* $(,)?) => {{
let mut map = ::std::collections::HashMap::with_capacity($crate::count!($($k),*));
$( map.insert($k, $v); )*
map
}};
}
// 比較用: with_capacity を付けない版
macro_rules! hashmap_nocap {
($($k:expr => $v:expr),* $(,)?) => {{
let mut map = ::std::collections::HashMap::new();
$( map.insert($k, $v); )*
map
}};
}
// ---------- 課題3: impl_checksum! — ty フラグメントで8型に独立した impl ----------
pub trait Checksum {
/// リトルエンディアンのバイト列を足し合わせた 8bit チェックサム
fn checksum(&self) -> u8;
}
macro_rules! impl_checksum {
($($t:ty),+ $(,)?) => {
$(
impl Checksum for $t {
fn checksum(&self) -> u8 {
self.to_le_bytes().iter().fold(0u8, |acc, b| acc.wrapping_add(*b))
}
}
)+
};
}
impl_checksum!(u8, u16, u32, u64, i8, i16, i32, i64);
// ---------- 課題4: newtype! — meta / vis / ident / ty ----------
macro_rules! newtype {
($(#[$m:meta])* $vis:vis struct $name:ident($inner:ty);) => {
$(#[$m])*
$vis struct $name($inner);
impl $name {
$vis fn new(v: $inner) -> Self { Self(v) }
$vis fn into_inner(self) -> $inner { self.0 }
}
impl ::std::convert::From<$inner> for $name {
fn from(v: $inner) -> Self { Self(v) }
}
};
}
mod ids {
newtype! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(u64);
}
newtype! {
#[derive(Debug)]
struct Secret(String); // vis 無し = このモジュール内だけ
}
pub fn secret_len() -> usize {
Secret::new("hunter2".to_string()).into_inner().len()
}
}
use ids::UserId;
const N_KEYS: usize = count!(a, b, c, d);
const N_REC: usize = count_rec!(a, b, c, d);
fn main() {
println!("count!(a, b, c, d) = {N_KEYS}, count_rec! = {N_REC}");
println!("count!() = {}", count!());
let m = hashmap! { "apple" => 3, "banana" => 5, "cherry" => 7, };
let mut keys: Vec<_> = m.keys().copied().collect();
keys.sort();
println!("hashmap! keys = {keys:?}, len = {}, capacity >= len: {}", m.len(), m.capacity() >= m.len());
let dup = hashmap! { "k" => 1, "k" => 2 };
println!("duplicate key: len = {}, k = {}", dup.len(), dup["k"]);
let empty: HashMap<&str, i32> = hashmap! {};
println!("empty: len = {}, capacity = {}", empty.len(), empty.capacity());
// ヒープ確保回数の比較(100要素)
let (_, with_cap) = allocs_during(|| hashmap! {
0=>0,1=>1,2=>2,3=>3,4=>4,5=>5,6=>6,7=>7,8=>8,9=>9,10=>10,11=>11,12=>12,13=>13,14=>14,15=>15,16=>16,17=>17,18=>18,19=>19,
20=>20,21=>21,22=>22,23=>23,24=>24,25=>25,26=>26,27=>27,28=>28,29=>29,30=>30,31=>31,32=>32,33=>33,34=>34,35=>35,36=>36,37=>37,38=>38,39=>39,
40=>40,41=>41,42=>42,43=>43,44=>44,45=>45,46=>46,47=>47,48=>48,49=>49,50=>50,51=>51,52=>52,53=>53,54=>54,55=>55,56=>56,57=>57,58=>58,59=>59,
60=>60,61=>61,62=>62,63=>63,64=>64,65=>65,66=>66,67=>67,68=>68,69=>69,70=>70,71=>71,72=>72,73=>73,74=>74,75=>75,76=>76,77=>77,78=>78,79=>79,
80=>80,81=>81,82=>82,83=>83,84=>84,85=>85,86=>86,87=>87,88=>88,89=>89,90=>90,91=>91,92=>92,93=>93,94=>94,95=>95,96=>96,97=>97,98=>98,99=>99,
});
let (_, no_cap) = allocs_during(|| hashmap_nocap! {
0=>0,1=>1,2=>2,3=>3,4=>4,5=>5,6=>6,7=>7,8=>8,9=>9,10=>10,11=>11,12=>12,13=>13,14=>14,15=>15,16=>16,17=>17,18=>18,19=>19,
20=>20,21=>21,22=>22,23=>23,24=>24,25=>25,26=>26,27=>27,28=>28,29=>29,30=>30,31=>31,32=>32,33=>33,34=>34,35=>35,36=>36,37=>37,38=>38,39=>39,
40=>40,41=>41,42=>42,43=>43,44=>44,45=>45,46=>46,47=>47,48=>48,49=>49,50=>50,51=>51,52=>52,53=>53,54=>54,55=>55,56=>56,57=>57,58=>58,59=>59,
60=>60,61=>61,62=>62,63=>63,64=>64,65=>65,66=>66,67=>67,68=>68,69=>69,70=>70,71=>71,72=>72,73=>73,74=>74,75=>75,76=>76,77=>77,78=>78,79=>79,
80=>80,81=>81,82=>82,83=>83,84=>84,85=>85,86=>86,87=>87,88=>88,89=>89,90=>90,91=>91,92=>92,93=>93,94=>94,95=>95,96=>96,97=>97,98=>98,99=>99,
});
let (_, from_arr) = allocs_during(|| {
let arr: [(i32, i32); 100] = std::array::from_fn(|i| (i as i32, i as i32));
HashMap::from(arr)
});
println!("allocs for 100 entries: hashmap! = {with_cap}, no with_capacity = {no_cap}, HashMap::from([..;100]) = {from_arr}");
println!("checksum: 0x0102u16 = {}, -1i32 = {}, 300u64 = {}", 0x0102u16.checksum(), (-1i32).checksum(), 300u64.checksum());
let id = UserId::new(42);
let id2: UserId = 42.into();
println!("newtype: {:?} == {:?} -> {}, inner = {}, secret_len = {}", id, id2, id == id2, id.into_inner(), ids::secret_len());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_is_a_const() {
const N: usize = count!(x, y, z);
let arr = [0u8; N]; // 配列長に使える = コンパイル時定数
assert_eq!(arr.len(), 3);
assert_eq!(count!(), 0);
assert_eq!(count!(a,), 1);
assert_eq!(count!((1, 2), [3, 4], { 5 }), 3); // 括弧のまとまりは1個の tt
}
#[test]
fn count_matches_recursive_version() {
assert_eq!(count!(a, b, c, d, e), count_rec!(a, b, c, d, e));
}
#[test]
fn hashmap_basic_and_trailing_comma() {
let m = hashmap! { "a" => 1, "b" => 2, };
assert_eq!(m.len(), 2);
assert_eq!(m["a"], 1);
let m2 = hashmap! { "a" => 1 };
assert_eq!(m2["a"], 1);
}
#[test]
fn hashmap_empty_needs_annotation() {
let m: HashMap<String, u8> = hashmap! {};
assert!(m.is_empty());
}
#[test]
fn hashmap_duplicate_key_last_wins() {
let m = hashmap! { "k" => 1, "k" => 2 };
assert_eq!(m.len(), 1);
assert_eq!(m["k"], 2);
}
#[test]
fn hashmap_evaluates_each_expr_once_in_order() {
let log = Cell::new(0u32);
let tick = |tag: u32| { log.set(log.get() * 10 + tag); tag };
let m = hashmap! { tick(1) => tick(2), tick(3) => tick(4) };
assert_eq!(log.get(), 1234); // k1, v1, k2, v2 の順に1回ずつ
assert_eq!(m.len(), 2);
}
#[test]
fn hashmap_local_name_is_hygienic() {
let map = "caller's map";
let m = hashmap! { map => 1 }; // マクロ内の `map` と衝突しない
assert_eq!(m["caller's map"], 1);
}
#[test]
fn hashmap_preallocates() {
let (_, n) = allocs_during(|| hashmap! { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8 });
assert_eq!(n, 1);
}
#[test]
fn checksum_all_eight_types() {
assert_eq!(0xFFu8.checksum(), 0xFF);
assert_eq!(0x0102u16.checksum(), 3);
assert_eq!((-1i32).checksum(), 0xFC); // FF*4 = 0x3FC → 下位 8bit
assert_eq!(300u64.checksum(), 45); // 0x012C → 0x2C + 0x01
assert_eq!((-1i8).checksum(), 0xFF);
assert_eq!(1i16.checksum(), 1);
assert_eq!(1u32.checksum(), 1);
assert_eq!(1i64.checksum(), 1);
}
#[test]
fn newtype_generates_struct_and_from() {
let a = UserId::new(7);
let b: UserId = 7u64.into();
assert_eq!(a, b);
assert_eq!(a.into_inner(), 7);
assert_eq!(ids::secret_len(), 7);
}
}実測(出題者の環境: rustc 1.96.0 / cargo 1.96.0 / edition 2024 / Apple M1 8GB / macOS 26.5.1、2026-09-27)
cargo run -q の出力(実測):
count!(a, b, c, d) = 4, count_rec! = 4
count!() = 0
hashmap! keys = ["apple", "banana", "cherry"], len = 3, capacity >= len: true
duplicate key: len = 1, k = 2
empty: len = 0, capacity = 0
allocs for 100 entries: hashmap! = 1, no with_capacity = 6, HashMap::from([..;100]) = 1
checksum: 0x0102u16 = 3, -1i32 = 252, 300u64 = 45
newtype: UserId(42) == UserId(42) -> true, inner = 42, secret_len = 7
cargo test -q → test result: ok. 10 passed; 0 failed(実測)。cargo clippy -q --all-targets の警告は 0 件(実測)。
計測1: 再帰の count_rec! は 128 個で壊れる(実測。rustc --edition 2024 で単体ファイルをビルド)
| 要素数 | count_rec!(再帰) | count!(スライス長) |
|---|---|---|
| 100 | 100 | — |
| 127 | 127 | — |
| 128 | エラー: recursion limit reached | — |
| 200 | エラー | 200 |
| 10,000 | (試していない) | 10000 |
count_rec! は「要素が0個になるまで」自分を呼ぶので、128 要素なら 129 段の展開が要ります。既定の recursion_limit は 128 なので、127 要素が上限でした。
計測2: 上限を上げれば再帰版も通るが、コンパイル時間が伸びる(実測、各3回、real 秒)
#![recursion_limit = "4096"] を付けて再帰版もコンパイルできるようにし、rustc 1回の時間を比べました。
| 要素数 | count_rec!(3回) | 中央値 | count!(3回) | 中央値 |
|---|---|---|---|---|
| 1,000 | 1.26 / 0.29 / 0.27 | 0.29 | 0.17 / 0.12 / 0.13 | 0.13 |
| 2,000 | 0.89 / 0.68 / 0.83 | 0.83 | 0.34 / 0.15 / 0.14 | 0.15 |
要素数を2倍にしたとき、再帰版の中央値は 0.29 → 0.83 秒(約2.9倍)、スライス版は 0.13 → 0.15 秒でした。再帰版は1段ごとに「残り全部」を照合し直すので、照合するトークン数が要素数の2乗に比例して増える(導出)ためです。ただしこの機械はメモリ 8GB でスワップが多く、1回目の値が大きくぶれています(1.26 秒など)。桁の傾向だけを読み、小数の差には意味を持たせないでください。
計測3: ヒープ確保の回数(100 要素、i32 => i32、実測)
| 作り方 | 確保回数(alloc + realloc) |
|---|---|
hashmap!(with_capacity(100)) | 1 |
with_capacity なしで insert を 100 回 | 6 |
HashMap::from([(k, v); 100]) | 1 |
with_capacity なしでは、表が一杯になるたびに作り直すので6回確保しました。一方 HashMap::from も1回でした。HashMap::from は内部で要素数を知ってから確保するためです(FromIterator が反復子の size_hint で先に確保する、が理由の推定。std のソースは今日は読んでいない)。つまり hashmap! の with_capacity は、HashMap::from に対する利点ではありません(D2 の答えにつながる)。
D1 の回答: count! はスライス長の流儀を採る
再帰(count_rec!) | スライス長(count!) | |
|---|---|---|
| 128 要素以上 | 既定ではコンパイルエラー(計測1) | 通る(10,000 まで実測) |
| コンパイル時間(2,000 要素) | 0.83 秒(中央値) | 0.15 秒(中央値) |
const で使えるか | 使える(1usize + 1usize + ...) | 使える(<[()]>::len は const fn) |
| 読みやすさ | 直感的 | <[()]>::len の意味を知らないと読めない |
- 採った理由: 上限に当たらないことと、コンパイル時間が要素数にほぼ比例しないこと。
hashmap!のような利用者が要素数を決めるマクロでは、128 個を超える呼び出しが来ないとは言えません(設定値の表・エラーコード表など) - 採らなかった理由: 再帰版の「
recursion_limitを上げればよい」は、マクロの利用者のクレートに#![recursion_limit]を書かせることになります(上限はマクロを定義した側ではなく、展開が起きるクレートの属性で決まる)。利用者に内部事情を漏らす設計は避けます - 前提が変わったら: 要素数が必ず小さい(例: 8型ぶんの
implなど)内部専用のマクロなら、読みやすさを優先して再帰でもよい
D2 の回答: チームの標準は HashMap::from にし、hashmap! は作らない
判断は迷うところですが、今日の計測3で with_capacity の差が消えたので、std で済むなら std を採ります。
| 観点 | hashmap!(今日の自作) | HashMap::from([...])(Rust 1.56 から std) |
|---|---|---|
| 確保回数 | 1(計測3) | 1(計測3) |
| 見た目 | "a" => 1 | ("a", 1) |
| 保守 | 自作マクロのテスト・ドキュメントが要る | 不要 |
| 0要素 | hashmap!{} が書ける | HashMap::from([]) は型注釈が必須で、HashMap::new() の方が自然 |
| 要素の型の不一致 | エラーが insert の行を指す(下のエラー4) | エラーが配列リテラルを指す |
| 値を1回だけ評価 | マクロの書き方しだい(要件3。テストで保証) | 関数の引数なので構造的に保証 |
- 採らなかった案: 既存のクレート
maplit(hashmap!btreemap!などを提供)を入れる案。依存を1つ増やす利点が=>の見た目だけなので見送り - 前提が変わったら:
=>の見た目がドメイン上の意味を持つ(例: 状態遷移表Idle => Runningを大量に書く)なら、hashmap!ではなくその表専用の DSL マクロを作る。これは Phase 4 #8「マクロによる DSL 設計」で扱います
🪜 Step-by-Step 解説
count! — 再帰を「並べて長さを聞く」に置き換える
macro_rules! count {
(@unit $_t:tt) => { () };
($($x:tt),* $(,)?) => { <[()]>::len(&[$($crate::count!(@unit $x)),*]) };
}
count!(a, b, c) は <[()]>::len(&[(), (), ()]) に展開されます。count!(@unit $x) は、繰り返しの中で1段ずつ呼ばれるだけで、ネストはしません。だから展開の深さは要素数に関係なく2段で、recursion_limit に当たりません(計測1)。
<[()]>::lenとスライス型を明示するのは、0要素のときに&[]の要素型を決めるためです。要素型を書かずに[$( ... ),*].len()とすると、count!()が[].len()に展開されて E0282(type annotations needed) になります(実測)。<[()]>::len(&[])なら要素型が()に決まり、count!()→0になります(テストで確認)()は大きさ 0 の型なので、この配列は実行時に何も確保しません。そもそもconstの文脈で評価されればコンパイル時に 3 という定数になります@unitの規則は上に置きます。規則は上から順に試されるので、内部規則を下に置くと、公開用の規則が@unit xを先に食べてしまう形のマクロでは内部規則に届きません(今日のcount!はカンマ区切りを要求するので@unit xは2番目の規則に一致しませんが、内部規則は上を慣習にしておくと事故がありません)
hashmap! の要件を1つずつ潰す
| 要件 | 書き方 | 根拠 |
|---|---|---|
| 1. 末尾カンマ | $(,)? | 昨日 Q8 |
| 2. 一度だけ確保 | with_capacity($crate::count!($($k),*)) | 計測3で確保1回 |
| 3. 1回ずつ評価 | $k $v を $( map.insert($k, $v); )* の1か所にだけ書く | テスト hashmap_evaluates_each_expr_once_in_order で順序 1234 |
4. map が衝突しない | マクロ内の let mut map はローカル変数なので衛生的 | テスト hashmap_local_name_is_hygienic |
5. use 不要 | ::std::collections::HashMap と $crate::count! | 下のエラー3 |
| 6. 0要素で警告なし | () => { ::std::collections::HashMap::new() }; を先に置く | 下の「警告」 |
要件2で注意したいのは、count! には $k を渡しているのに $k が2回評価されないことです。count! の中で $x は () に置き換えられて捨てられるので、キーの式(tick(1) など)は count! の展開結果に一度も残りません。評価されるのは insert の1か所だけです。昨日の Q4 は「トランスクライバに2回書くと2回評価される」でしたが、正確には「展開結果に2回残ると2回評価される」です。
要件6は、実装してみて初めて出た警告です。0要素の hashmap!{} を2番目の規則だけで受けると、展開結果は
{ let mut map = ::std::collections::HashMap::with_capacity(<[()]>::len(&[])); map }
になり、map を一度も変更しないので unused_mut の警告が呼び出し側に出ます(実測。下の「警告」)。#[allow(unused_mut)] で黙らせる手もありますが、0要素の規則を先に置く方が、展開結果が HashMap::new() と素直になります。
impl_checksum! — ty で受けて型ごとに独立した impl
macro_rules! impl_checksum {
($($t:ty),+ $(,)?) => { $( impl Checksum for $t { ... } )+ };
}
$t:ty で受けた型は、impl Checksum for $t の型の位置に型の塊として埋め込まれます。to_le_bytes() は整数型ごとに戻り値の配列の長さが違う(u8 は [u8; 1]、u64 は [u8; 8])ので、ジェネリックな impl<T: ...> Checksum for T では書けません(それを表すトレイトが std に無い)。マクロなら型ごとに別の impl を作るので、各 impl の中では to_le_bytes が具体的な型のメソッドとして解決されます。
繰り返しは +(1回以上)にしました。impl_checksum!() を空で呼ぶ意味は無いので、誤用をコンパイルエラーにするためです。
newtype! — vis と meta で呼び出し側の決定を転記する
$vis:visはpub・pub(crate)・pub(super)・空のどれにも一致します。$($vis:vis)?と書くと、定義の時点でerror: repetition matches empty token tree(note: a vis fragment can already be empty)になります(実測)$(#[$m:meta])*は#[derive(...)]や#[doc = "..."]を何個でも受けて、$(#[$m])*でそのまま構造体の上に戻します。マクロがderiveの中身を知らなくてよいので、呼び出し側がHashを足したければ足せます- 生成した
Secretはvisが空なのでidsモジュールの外からは見えません(下のエラー2)。newとinto_innerにも同じ$visを付けたので、「構造体は非公開なのにnewだけpub」のような不整合が起きません From<$inner>のimplには$visを付けていません。トレイトのimplには可視性が無い(型とトレイトの両方が見える場所なら使える)からです
最初はカウンタを static ALLOCS: AtomicUsize にしました。cargo run では 100 要素で1回と出たのに、cargo test の hashmap_preallocates(8要素で1回を期待)は 9 で失敗しました(実測)。cargo test は既定でテストを並列スレッドで実行するので、他のテストが同時に行った確保まで数えていたのです。
カウンタを thread_local! に移して解決しました。const { Cell::new(0) } で初期化しておくと、スレッドローカル変数の初期化自体がヒープを確保しないので、アロケータの中から安全に触れます。別解は cargo test -- --test-threads=1 ですが、テストが実行方法に依存するので採りませんでした。
🧭 設計思想・なぜこう書くのか
マクロは「構文の個数」を扱い、型の仕事は型に任せる
今日の4つのマクロはどれも、型システムでは書けない部分だけをマクロにしています。
| マクロ | マクロにしか書けない部分 | 型・関数に任せた部分 |
|---|---|---|
count! | 個数を数える(可変個の引数) | 数えた結果は usize の定数 |
hashmap! | => の区切りと可変個 | 型の一致は HashMap::insert のシグネチャが検査(エラー4) |
impl_checksum! | 型ごとに独立した impl を並べる | バイト列の和の計算は普通の関数呼び出し |
newtype! | 構造体の名前・可視性・属性を受け取って項目を作る | derive の中身はコンパイラの derive に任せる |
判断の記録: 今日の問題の作り方
| 判断 | 内容 |
|---|---|
| 採った案 | 4つのマクロ+2つの設計設問。数え方・確保回数・コンパイル時間を実測し、設計設問の答えを実測で決めた |
迷った案1: hashmap! だけを深く掘る | 昨日の「扱わなかった範囲」で ty ident vis を使うと予告していたので、impl_checksum! と newtype! を足した。フラグメント指定子 15 種のうち、昨日の5種に今日 ty vis meta の3種を足して累計8種になる |
迷った案2: count! を tt マンチャ(1トークンずつ食べる再帰)で書く | 再帰の上限に当たる点は count_rec! と同じ。tt マンチャはマクロ① の後半の DSL の回に回した |
| 迷った案3: ベンチマーク(criterion)で挿入速度を測る | 確保回数は決定的に数えられる(何度測っても 1 と 6)が、挿入の速度は 8GB の機械ではスワップのぶれが大きい。今日はぶれない指標(確保回数)を選び、速度は測っていない |
| 出題者が実装して初めて分かったこと | 0要素で unused_mut 警告が出る(要件6)、並列テストでカウンタが汚れる(Step 5)。どちらも問題文の要件と Step に取り込んだ |
| 前提が変わったら | macro_rules! に要素数を直接数える機能(nightly の ${count(x)} などのメタ変数式)が安定化すれば、count! 自体が不要になる。2026-09-27 時点の stable(1.96.0)では使えないことを確かめた(下のエラー5) |
🚨 コンパイルエラーが出た場合
以下はすべて出題者の環境(rustc 1.96.0、edition 2024、aarch64 macOS)で実際に出た出力です(色コードは除去)。
警告: 0要素の規則が無い hashmap!(要件6)
読み方: 警告はマクロの定義の行を指し、in this macro invocation でどの呼び出しで起きたかを示します。呼び出し側では直せない警告なので、マクロの作者が直すべきものです。
エラー1: 0要素で型が決まらない(E0282)
let m = hashmap! {}; // 型注釈なし
println!("{}", m.len());
これはマクロの不具合ではなく、HashMap::new() と同じです。キーと値の型を推論する材料が無いので、呼び出し側で let m: HashMap<&str, i32> と書きます。
エラー2: vis を空にした構造体を外から使った(E0603・E0624)
UserId(pub)は通り、Secret(空)は外から使えません。$vis が期待どおりに転記されている証拠です。note がマクロ定義の中の $vis struct $name($inner); を指している点も読みどころで、マクロが生成した項目のエラーは、定義の行と呼び出しの行の両方が出ることを覚えておくと追いやすくなります。
エラー3: マクロの中で HashMap を裸で書いた(E0433、要件5)
help は「use を足せ」と言いますが、マクロの作者がこれに従ってはいけません。足すべきなのは呼び出し側ではなく、マクロ内の絶対パス ::std::collections::HashMap です。型名は呼び出し側で解決される(項目は衛生的でない)ので、定義側のクレートで use していても、利用者のクレートでは見えません。
エラー4: 値の型がそろっていない(E0308)
let m = hashmap! { "a" => 1, "b" => "two" };
(実際の出力はこの後にマクロ定義の行を指す help が 15 行ほど続きます。)型の検査はマクロではなく、展開後の map.insert の呼び出しで行われます。マクロは型を知らないので、型エラーは必ず展開後のコードの言葉で出る、が macro_rules! の限界です(型を見て分岐したいなら proc-macro か、そもそもトレイトで書く)。
エラー5: nightly のメタ変数式 ${count(x)} は stable では使えない
macro_rules! cnt { ($($x:tt),*) => { ${count($x)} }; }
${count(x)} のような「メタ変数式」が安定化すれば count! は要らなくなりますが、rustc 1.96.0 の stable では E0658 で使えません(実測)。追跡 issue #83527 が閉じたら、count! を置き換えられるか見直します。
🌐 他言語との比較
| 観点 | Go | Java | C++ | TypeScript | Rust |
|---|---|---|---|---|---|
| マップのリテラル | map[string]int{"a": 1}(言語の構文) | Map.of("a", 1)(10組まで。以上は Map.ofEntries) | std::unordered_map<std::string,int>{{"a",1}}(初期化子リスト) | new Map([["a", 1]]) | 言語の構文には無い。HashMap::from([...]) か hashmap! |
| 事前確保 | make(map[string]int, n) を手で | HashMap(initialCapacity) を手で | reserve(n) を手で | なし | with_capacity(n)。マクロなら要素数を自動で数えられる |
| 型ごとの同じ実装 | ジェネリクス+型制約(1.18〜)か go generate | ジェネリクス(プリミティブ型は使えない。int は Integer に箱詰め) | テンプレート | ジェネリクス(実行時は型が消える) | macro_rules! で型ごとに別の impl(課題3) |
| newtype | type UserID uint64(言語の構文、変換は明示) | クラスを書く/record UserId(long v) | struct を書く/enum class | ブランド型 number & { __brand: "UserId" } | タプル構造体。付随する impl の量産を newtype! で |
Go は map[string]int{...} という言語の構文でこの問題を解いたので、マクロが要りません。Rust は言語にマップのリテラルを入れず、std の関数(HashMap::from)とマクロ(利用者が作る)に任せました。Java の Map.of が10組までという固定の上限を持つのは、可変長の型安全な引数を作る仕組みが無いため引数の個数ごとにオーバーロードを並べているからで、Rust の macro_rules! の繰り返しはこの問題をコンパイル時に解いています。
🏭 実務での使いどころ
- 設定値・コード表の初期化: HTTP ステータスや エラーコード → メッセージの表。要素数が 128 を超えることは普通にあるので、
count!をスライス長で書く理由がここにある(計測1) - 数値型への一括
impl: std 自身が整数型のメソッドやFromをマクロで量産している。自前のシリアライザ・チェックサム・単位変換で同じ形が出る - newtype の量産:
UserId(u64)・OrderId(u64)・TenantId(u64)のように取り違えを型で防ぐ ID 型を何十個も作るとき。newtype!1行でFrom・new・into_innerがそろう。実務ではさらにserdeの#[serde(transparent)]をmetaで渡す #[global_allocator]による確保回数の計測: 今日の計測用アロケータはそのまま、ホットパスで「確保0回」を保証するテストに使える(dhatクレートは同じ考え方でより詳しい統計を出す)
レビューで見る点: #[macro_export] するマクロの中のパスがすべて ::std::... か $crate::... か、0要素で展開結果が警告・エラーを出さないか、再帰するマクロに要素数の上限が暗黙に入っていないか。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
再帰で数える count! で十分 | 手元のテストは数個の要素でしか呼ばない | 既定の recursion_limit 128 により 127 要素が上限(計測1)。利用者側の #![recursion_limit] で回避させるのは内部事情の漏えい |
count!($($k),*) にキーを渡すとキーが2回評価される | 「メタ変数を2回書くと2回評価」を字面で覚えている | 評価されるのは展開結果に残った回数。count! は $x を () に置き換えて捨てるので、評価は insert の1回だけ(テストで順序 1234 を確認) |
hashmap! の with_capacity は HashMap::from より速い | 自作マクロの工夫を過大評価する | 100 要素で確保回数はどちらも1回(計測3)。利点は => の見た目だけ |
| 0要素でも1つの規則で受ければよい | 繰り返し * が0回を許すので安心する | let mut map が変更されず unused_mut 警告が呼び出し側に出る(実測)。0要素の規則を先に置く |
$vis:vis を $($vis:vis)? と書く | 「省略可能なもの」は ? で囲む癖 | vis は空にも一致するので、? で囲むと定義エラー repetition matches empty token tree(実測) |
確保回数のテストを static AtomicUsize で書く | cargo run では正しく数えられたので安心する | cargo test は並列スレッドで走るので他のテストの確保が混ざる(実測: 1 のはずが 9)。thread_local! に置く |
🔗 一次資料(参照日: 2026-09-27。記述は各資料の要旨で、原文の引用ではありません)
- The Rust Reference — Macros By Example(フラグメント指定子
tyidentvismeta、visが空に一致すること、follow-set 制限): https://doc.rust-lang.org/reference/macros-by-example.html - The Rust Reference — Limits:
recursion_limit(既定値 128): https://doc.rust-lang.org/reference/attributes/limits.html - The Little Book of Rust Macros — Counting(再帰・スライス長などの数え方の比較): https://veykril.github.io/tlborm/decl-macros/building-blocks/counting.html
- std:
HashMap::with_capacity・impl From<[(K, V); N]> for HashMap(Rust 1.56 で安定化): https://doc.rust-lang.org/std/collections/struct.HashMap.html - std:
GlobalAllocと#[global_allocator]: https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html - 既存の実物:
maplitクレート(hashmap!などのマップのリテラルマクロ): https://crates.io/crates/maplit
🚧 今日扱わなかった範囲(次回以降)
| 扱わなかったもの | 理由・予定 |
|---|---|
| 挿入速度のベンチマーク | 8GB の機械ではぶれが大きく、確保回数(決定的)を代わりに測った。測るなら criterion で |
HashMap::from が1回で確保する理由を std のソースで確かめる | 今日は計測結果のみ。library/std/src/collections/hash/map.rs の From<[(K, V); N]> と FromIterator の実装を読めば確かめられる |
メタ変数式 ${count(x)} を nightly で動かす | stable で E0658 になることだけ確認(エラー5)。nightly が手元に無い |
別クレートからの $crate の実測(要件5の本当の確認) | 今日は同じクレートの中でのみ確認。ワークスペースで2クレートにする回(#[macro_export] の回)にまとめる |
フラグメント指定子の残り7種(pat pat_param path item lifetime literal stmt) | 累計 15 種中8種を使用。残りは DSL の回で pat literal を中心に使う |
| tt マンチャ・内部規則を使った再帰的な構文解析 | マクロ① の後半(DSL を1つ書く回) |
➡️ 次のステップ
- 発展:
newtype!に$(, derive_serde)?のような省略可能な指示を足して、指定されたときだけimpl Displayも生成する。規則を2つに分けるか、内部規則@displayを使うか比べる - 次回予告: マクロ① の3問目(デバッグ・コードレビュー)— 「動くが壊れやすい」
macro_rules!を 6 か所読んで直す(評価回数・絶対パス・follow-set・0要素・再帰上限・衛生性)