値からキーを借用できる Rust の特殊マップ型クレート「iddqd」— IdOrdMap・BiHashMap・TriHashMap を使いこなす
bash ファイル名.sh を実行してください(中身を一度確認してから実行すると安心です)。
(macOS / Linux 環境が必要) 値からキーを借用できる Rust の特殊マップ型クレート「iddqd」— IdOrdMap・BiHashMap・TriHashMap を使いこなす
ひとことでいうと
iddqd(読み方:イッド・クォッド)は、Rust のプログラムで使える特殊なマップ型(辞書のようなデータ構造)を提供するライブラリです。通常の HashMap では「キー」と「値」を別々に渡して管理しますが、iddqd では値の中にキーが含まれている場合、そのキーを値から自動的に取り出して使えます。キーの二重管理が不要になり、データのズレや管理の手間を型レベルで防げます。Oxide Computer(大規模 Rust システムを手がける企業)が実際の開発現場で生まれた課題をもとに作ったクレートで、実用性が高いのが特徴です。名前はゲーム「DOOM」の有名なチートコード「IDDQD」に由来しています。
このソフトで何ができる?
iddqd は用途に合わせて選べる 4 種類のマップ型を提供します。
IdOrdMap: B-Tree(木構造)ベースの順序付きマップです。キー順にデータを並べて取り出せます。IdHashMap: ハッシュマップ(高速な辞書型)です。デフォルトのハッシュ計算にはfoldhashを使用しており、標準の SipHash より高速ですが、意図的な攻撃(HashDoS)への耐性は低くなっています。BiHashMap: 2 つのキーで同じ値を管理する双方向マップです。どちらのキーからも O(1)(一定時間)で検索できます。bijection(全単射)とも呼ばれる 1 対 1 対応の関係を扱います。TriHashMap: 3 つのキーで値を管理する三方向マップです。どのキーからでも O(1) で検索できます。trijection(1:1:1 の対応関係)と呼ばれます。
すべてのマップ型で共通する特徴として、insert の代わりに insert_unique(重複時はエラー)と insert_overwrite(重複時は上書き)を明示的に選ぶ API 設計を採用しています。何が起きるかが一目でわかり、意図しない挙動を防げます。
こんな人におすすめ
1. 複数フィールドの組み合わせをキーにしたい Rust 開発者
struct(構造体)の複数フィールドを組み合わせた複合キーで検索したい場面があります。標準の HashMap では Borrow トレイトの制約が壁になりがちです。iddqd は Equivalent トレイトにより、所有型と借用型が混在するルックアップを自然に書けます。設計の複雑さが大幅に下がります。
2. キーと値の二重管理をなくしてデータの一貫性を保ちたい設計者
マイクロサービスや設定管理の実装では、「値の中にキーが含まれているのに、マップのキーとして別途渡す」設計はデータの不整合を招きやすいです。iddqd はこの冗長性をトレイト実装で吸収し、型レベルで整合性を保証してくれます。
3. 双方向・三方向のマッピングが必要な場面
数値 ID から名前へ、名前から ID へと相互に検索したい場合、BiHashMap は 2 方向から、TriHashMap は 3 方向から O(1) でアクセスできます。標準ライブラリで同じことをやろうとすると、複数の HashMap を手動で同期させる複雑なコードが必要になります。
インストール・使い方
Step 1: Cargo.toml に依存を追加する
Cargo.toml(プロジェクトの設定ファイル)に以下を追記します。
[dependencies]
iddqd = "0.4.3"
JSON や YAML への変換(シリアライズ)が必要な場合は、serde フィーチャーを有効にします。
[dependencies]
iddqd = { version = "0.4.3", features = ["serde"] }
ターミナル(文字で命令を送る画面)からコマンドで追加する方法もあります。コピー&ペーストで実行できます。
cargo add iddqd
# serde 対応が必要な場合
cargo add iddqd --features serde
cargo add は Rust のパッケージ管理コマンドです。実行後、Cargo.toml が自動的に更新されます。
Step 2: 値型にトレイトを実装する
「値の中のどのフィールドをキーとして使うか」をトレイト(機能の定義)でマップに伝えます。
use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
#[derive(Debug)]
struct User {
name: String,
age: u8,
}
// IdOrdItem を実装することで、マップがキーの取り出し方を知る
impl IdOrdItem for User {
type Key<'a> = &'a str;
fn key(&self) -> Self::Key<'_> {
&self.name // name フィールドをキーとして借用する
}
id_upcast!(); // 定型マクロ(必須)
}
impl IdOrdItem for User の部分は「User 型にキーの取り出し方を教える」手続きです。id_upcast!() は定型マクロで、コピーするだけで問題ありません。
Step 3: マップを操作する
let mut users = IdOrdMap::<User>::new();
// insert_unique: 重複キーがあれば Err を返す
users.insert_unique(User { name: "Alice".to_string(), age: 30 }).unwrap();
users.insert_unique(User { name: "Bob".to_string(), age: 35 }).unwrap();
// キー(&str)で O(log n) 検索
assert_eq!(users.get("Alice").unwrap().age, 30);
// 順序付きイテレーション
for user in &users {
println!("{}: {}", user.name, user.age);
}
insert_unique は重複するキーが存在すればエラーを返します。insert_overwrite を使えば既存の値を上書きします。どちらのメソッドも「挿入の意図を明示する」設計なので、挙動が曖昧になりません。
ブラウザで試す(デモ)
この記事にはインタラクティブなコードジェネレーターデモが付属しています。マップの種類(IdOrdMap / IdHashMap / BiHashMap / TriHashMap)と struct のフィールド名を入力すると、すぐにコピーして使えるサンプルコードが生成されます。インストールなしにブラウザ上でさまざまなマップ型の書き方を確認できます。まず動くコードの形を把握したい方はここから試すのがおすすめです。
動かしてみた
cargo add iddqd でプロジェクトに追加後、id_hash_map! マクロを使ったコレクションリテラル風の初期化が利用できます。以下は名前とバージョンの 2 フィールドを複合キーとして使うサンプルです。
use iddqd::{IdHashItem, id_hash_map, id_upcast};
#[derive(Debug)]
struct Artifact {
name: String,
version: String,
data: Vec<u8>,
}
#[derive(Eq, Hash, PartialEq)]
struct ArtifactKey<'a> {
name: &'a str,
version: &'a str,
}
impl IdHashItem for Artifact {
type Key<'a> = ArtifactKey<'a>;
fn key(&self) -> ArtifactKey<'_> {
ArtifactKey { name: &self.name, version: &self.version }
}
id_upcast!();
}
// id_hash_map! マクロで宣言的に初期化
let artifacts = id_hash_map! {
Artifact { name: "lib-a".to_owned(), version: "1.0".to_owned(), data: b"v1".to_vec() },
Artifact { name: "lib-b".to_owned(), version: "2.0".to_owned(), data: b"v2".to_vec() },
};
assert_eq!(
artifacts.get(&ArtifactKey { name: "lib-a", version: "1.0" }).unwrap().data,
b"v1",
);
id_hash_map! マクロを使うと、値を並べるだけでマップを初期化できます。キー型(ArtifactKey)と値型(Artifact)を別々に用意するのが複合キーを使うポイントです。
このクレートのテストは cargo-nextest(Rust 向けの高機能テストランナー)を使って実行します。Miri(unsafe コードを検証するツール)や proptest(ランダムなデータでテストする手法)など、品質保証の仕組みが充実しており、信頼性が高いクレートと言えます。
はじめの一歩 — すぐ試せる実践のコツ
最初に試すなら、IdHashMap に文字列フィールドを 1 つ持つシンプルな struct でのルックアップが最も理解しやすいです。以下のコードをそのまま src/main.rs に貼り付けて cargo run を実行してみてください。
use iddqd::{IdHashItem, IdHashMap, id_upcast};
struct Config {
key: String,
value: String,
}
impl IdHashItem for Config {
type Key<'a> = &'a str;
fn key(&self) -> &str { &self.key }
id_upcast!();
}
fn main() {
let mut map = IdHashMap::<Config>::new();
map.insert_unique(Config { key: "host".into(), value: "localhost".into() }).unwrap();
map.insert_unique(Config { key: "port".into(), value: "8080".into() }).unwrap();
println!("host = {}", map.get("host").unwrap().value);
println!("port = {}", map.get("port").unwrap().value);
}
cargo runはターミナルで実行する Rust のビルド&実行コマンドです。初めての方はこれだけ覚えておけば大丈夫ですIdHashItemの実装は「このフィールドがキーです」と宣言するだけで完了しますinsert_uniqueは重複を自動でエラーにしてくれるので、設定の誤上書きを防げます- 複合キーが必要になったら、専用のキー型(例:
ArtifactKey<'a>)を#[derive(Eq, Hash, PartialEq)]と一緒に定義する流れへ自然に移行できます
段階的に複雑さを増やしていける設計なので、最初はシンプルなところから始めましょう。
活用例・活用アイデア
- マイクロサービスのルーティングテーブル: サービス名・バージョン・リージョンの 3 フィールドをキーに持つエントリを
TriHashMapで管理すれば、任意の次元から O(1) 検索が可能になります。複数のHashMapを手動で同期させるコードが不要になり、保守コストが大幅に下がります。 - 設定ファイルのパーサ: キー名を値の中に保持する
Config型をIdHashMapで管理し、insert_uniqueによる重複設定キーの検出を型安全に実現できます。serde フィーチャーを有効にすれば、TOML や JSON をデシリアライズする際の重複キー拒否も自動で行われます。 - 双方向の ID ↔ 名前マッピング: データベースレコードの数値 ID と文字列スラッグ(URL 用の短縮名)を
BiHashMapで対応付ければ、どちらのキーからも O(1) アクセスが可能です。従来は 2 つのHashMapを手動で同期させる必要がありましたが、BiHashMapはその複雑さを 1 つのデータ構造にカプセル化します。 - ゲームのアセット管理: 画像や音声ファイルを数値 ID と文字列ラベルの両方から素早く検索したい場合に
BiHashMapが適しています。ゲームエンジンや UI フレームワークのような高パフォーマンスが求められる場面でも有効です。 - CI/CD パイプラインのジョブ管理: ジョブ名・実行環境・タグの 3 要素を
TriHashMapで管理することで、いずれかの属性からジョブを特定する処理をシンプルに記述できます。大規模なパイプラインでの検索コストを一定に保てます。 - 学習・ハンズオン用のデータ構造演習: 標準の
HashMapだけでは難しい複合キーや双方向マッピングの概念を、iddqdの明確な API で手を動かしながら学べます。Rust を学習中の方がデータ構造への理解を深める教材としても活用できます。
用語とポイント解説
IdOrdItem / IdHashItem / BiHashItem / TriHashItem
各マップ型に格納する値型が実装するトレイト(機能の定義)です。かんたんに言うと、「このデータ型はどのフィールドをキーとして使うか」をマップに伝えるための約束ごとです。実装するのは key() 関数と id_upcast!() マクロだけなので、慣れれば数行で書けます。マップの種類ごとに対応するトレイトが異なります(IdOrdMap なら IdOrdItem、IdHashMap なら IdHashItem など)。
id_upcast!()
トレイト実装の中に必ず書く定型マクロです。かんたんに言うと、Rust のライフタイム(変数の生存期間を管理する仕組み)に関する変換を自動で補ってくれるおまじないです。内部の仕組みを理解しなくても、コピーするだけで動きます。省略するとコンパイルエラーになるので、忘れずに記述しましょう。
Equivalent トレイト
標準の Borrow トレイトより柔軟な等価比較を提供するトレイトです。かんたんに言うと、「所有しているデータ型」と「借用しているデータ型」を同じキーとして扱うための橋渡しです。たとえば String(所有型)と &str(借用型)を同じキーとして比較できるようになります。複合キーでルックアップする際に特に重要な役割を果たします。
foldhash
IdHashMap のデフォルトで使われるハッシュアルゴリズムです。かんたんに言うと、データをマップのインデックスに変換する計算方法の一種です。標準の SipHash より高速に動作しますが、意図的な攻撃(同じハッシュ値を大量に生成する HashDoS 攻撃)への耐性は低くなっています。セキュリティが重要な場面では別のハッシャーへの切り替えを検討してください。
bijection(バイジェクション / 全単射)
BiHashMap の基礎となる数学的な概念です。かんたんに言うと「2 つの集合の要素が 1 対 1 で対応している状態」のことです。たとえば数値 ID とユーザー名が一対一で対応している場合、どちらからでも相手を一意に特定できます。BiHashMap はこの 1 対 1 の関係を型として保証します。
trijection(トリジェクション)
TriHashMap の基礎となる概念で、bijection を 3 方向に拡張したものです。かんたんに言うと「3 つの集合の要素が 1:1:1 で対応している状態」です。サービス名・バージョン・リージョンのような 3 次元の識別子を持つデータを管理する際に役立ちます。
insert_unique と insert_overwrite
iddqd のすべてのマップ型が持つ 2 種類の挿入メソッドです。かんたんに言うと、insert_unique は「重複があったらエラーを返す(安全優先)」、insert_overwrite は「重複があっても上書きする(上書き許可)」という違いがあります。標準の HashMap::insert のように「重複したとき何が起きるか曖昧」にならないよう、意図を明示して選ぶ設計になっています。
MSRV(Minimum Supported Rust Version / 最低対応 Rust バージョン)
クレートが動作するために必要な Rust の最低バージョンのことです。かんたんに言うと「このバージョン以上の Rust を使ってください」という目安です。iddqd の MSRV は 1.85 です。rustup update コマンドで Rust を最新版に更新しておけば、通常は問題なく利用できます。
ぜひ設定管理や双方向の ID マッピング、マイクロサービスのルーティングテーブルなどに活用してみてはいかがでしょうか。