---
name: d-good-parts
description: D言語のコードを書く・レビューするときに使う。テンプレート関数の属性や制約の付け方、trait の設計、mixin / 文字列 mixin の使い分け、Voldemort 型、Flag・Nullable・SumType 等の部品の選択、文字列（自動デコード）の罠、version 分岐の整理、OS 境界の書き方、CTFE の活用で迷うときに参照する。D 固有のイディオム集（良い部分と、避けるべき罠の両方）。
---

# D の good parts — Phobos に学ぶイディオム

D 固有の層だけを扱う。言語非依存の設計判断は分担先へ:
型能力の内省 → **design-by-introspection**、公開 API の形 → **api-ergonomics**、
性能技法 → **performance**。

## 1. テンプレートと属性

- **公開ジェネリック関数には必ず制約を書く**。制約は「古くならない仕様書」+「呼び出し
  地点でのエラー化」を同時に務める。制約がないとテンプレート本体の奥地の不可解な
  エラーを利用者が読まされる。
- **trait は厳密マッチで書く**。`isIntegral` が enum や暗黙変換にもマッチする v2 の設計は
  「制約は通ったのに期待した変換は起きない」亀裂を生み、v3 で厳密マッチに再設計された。
  「便利だから広めにマッチ」は制約の世界では**偽陽性**。緩い受け入れが必要なら
  trait に埋めず、使う側が `is(T : U)` で明示的に選ぶ。
- **属性（`@safe`/`pure`/`nothrow`/`@nogc`）は決め打ちせず推論に任せる**。明示すると
  利用者の述語・要素型がそれより緩いとき不当にコンパイルを拒否する。保証は
  **属性付き unittest**（`@safe pure nothrow @nogc unittest { ... }`）で代表的な
  インスタンス化を固定し、推論結果の後退を CI で検出する。
- **ステートレスな部品はメンバにせず alias に置き換える**。正典イディオム:

```d
static if (stateSize!Policy > 0) Policy policy;
else alias policy = Policy.instance;   // サイズ 0 — 合成を重ねても太らない
```

## 2. 型の道具箱（std.typecons 系の使いどころ）

- **Voldemort 型 + `auto`**: 戻り値型は関数内 private 構造体にして契約（Range である等）
  だけ公開。実装を自由に変えられる。代償（名指し不能）と出口は api-ergonomics 原則9。
- **`Flag!"name"` / `Yes.x` / `No.x`**: フラグごとに**異なる型**になるので引数の順序違いが
  コンパイルエラー。実体は `enum : bool` でコストゼロ。
- **`@disable this(this);` で所有権**: 「唯一の所有者」はコメントでなく言語機能で表現
  （`Unique`）。違反が実行時バグでなくコンパイルエラーになる。
- **値型オプショナルは union でデストラクタを制御**: union のメンバは自動破棄されない
  という言語仕様を使い、「空のときペイロードの dtor を呼ばない」を**構造で**保証する
  （`Nullable` の実装。フラグ分岐で dtor を避けるより堅い）。
- **`SumType` + `match`**: 網羅性検査がデフォルト、部分マッチは `tryMatch` という別名で
  オプトイン。`This` プレースホルダで再帰データ型（AST 等）も書ける。
- **型システムの実用上の穴は専用型で埋める**: 「参照先は const、参照自体は差し替えたい」
  → `Rebindable`（非 const 型には `alias` で透過＝コストゼロ）。strong typedef →
  `Proxy` mixin。利用者が `cast` で回避し始める前に、安全な抜け道を公式に置く。

## 3. mixin の規律 — 魔法は使ってよいが、漏らしてはならない

- **条件付きの定型は mixin template に括り出す**。「内包する Range が length を持つなら
  自分も持つ」をラッパーごとにコピペすると条件のズレ（`opDollar` だけ忘れる等）を生む。
  定義は一箇所・展開は型ごと:

```d
mixin template ImplementLength(alias member)
{
    static if (hasLength!(typeof(member)))
    {
        @property auto length() { return member.length; }
        alias opDollar = length;
    }
}
```

- **文字列 mixin は生成ロジックを型の内部に閉じ込め、利用者には普通の型として見せる**
  （`Tuple!(int, "x")` の名前付きフィールドは内部の文字列 mixin が `alias` を生成するが、
  利用者から見えるのはただの構造体）。生成された `t.x` と `t[0]` は同一実体への alias
  なのでコストもゼロ。
- **コンパイル時の語彙は実行時と同じにする**: 型リスト操作は `std.meta` の
  `AliasSeq` / `staticMap` / `Filter` / `allSatisfy` — 実行時の map/filter/all と同名・
  同意味論。メタプログラミングの学習コストの大半は語彙であり、「いつもの map を型に
  対してやるだけ」と理解できる形にする。

## 4. CTFE の二刀流

- **同じ関数をコンパイル時にも実行時にも動かす**。ポインタ操作や libc 呼び出しを含む
  高速パスは `if (!__ctfe)` でガードし、CTFE では同関数内の純 D 実装に落とす。
  CTFE 用の別 API を作らない。
- テーブルは `enum table = iota(256).map!(...).array;` のように CTFE 生成して焼き込む
  — 生成コードと生成物が同じソースにあり、外部生成スクリプトの同期ずれが消える。
- フォーマット文字列等は `format!"..."` のテンプレート引数版でコンパイル時検査
  （api-ergonomics 原則6 の D での形）。

## 5. 文字列の罠 — 自動デコード（bad part）

`char[]`/`string` を range として使うと**自動デコード**が働き、要素が `char` でなく
`dchar`（コードポイント）になる。これは v3 で公式に「失敗」と総括された仕様:

- `front` のたびに UTF-8 デコードが走り、不正な列で `UTFException` を投げうるため、
  文字列を触るだけでコードが `nothrow`/`@nogc` になれない（**属性汚染**）。
- `hasLength!(char[]) == false`、`isRandomAccessRange!(char[]) == false` —
  ジェネリックコードが string で突然遅い経路に落ちる。
- コストを払ってもコードポイントは人間の知覚する文字（グラフェム）ではなく、
  結合文字・絵文字は依然壊れる — **正しさの幻想**。

**処方**: バイト/コード単位処理は `byCodeUnit` か `representation`、本当の文字単位が
必要なら `byGrapheme` を明示。ジェネリックコードでは「狭い文字列か」を trait で分岐。
教訓として一般化すると「**暗黙のデフォルトは永遠** — opt-out 可能に見えても
エコシステムに浸透したデフォルトは撤去できない。迷ったら明示的 opt-in」。

## 6. OS 境界の書き方

- **字句操作とシステムコールをモジュールで分離**: `std.path`（純粋・CTFE 可能・
  割り当てゼロのスライス返し）と `std.file`（syscall）の分離。I/O に触らない層を
  モジュール境界で切ると、`pure`・CTFE・テスト容易性がモジュール単位で手に入る。
- **`version` 分岐は冒頭の alias 層に集約し、本体は1本にする**:

```d
version (CRuntime_Glibc)     private alias _FPUTC = fputc_unlocked;
else version (CRuntime_Microsoft) private alias _FPUTC = _fputc_nolock;
// 以降の実装は _FPUTC だけを使い、プラットフォームを知らない
```

  利用箇所に `version` をばら撒くと、プラットフォーム追加時に片方だけ直すバグを生む。
- **OS 資源は RAII（+ 参照カウント）で包み、`release()` の出口も用意**: ライブラリの
  管理から外してハンドルを他へ渡す正規の手段がないと、利用者はデストラクタと戦う。
- **OS の罠の知識を API に焼き込む**: `exists` が `access(2)` でなく `lstat` を使うのは
  SUID プログラムで実効 UID 判定を誤るから。`SO_NOSIGPIPE` の自動設定、Windows 引数
  クォートの正確な実装 — 「正しい使い方を知っている人が一度だけ書き、全員が使う」。
  罠の理由はコメントに残す。

## 7. v2 の失敗から（負の教訓）

- **モジュールは1主題**。`std.typecons`（Tuple・Nullable・Flag・RefCounted の同居）は
  「型の便利置き場」という主題のない grab-bag で、v3 で分割判定。良い部品を作ることと
  どの箱に入れるかは別の設計判断。
- **単一入口は1〜2回のディスパッチで実装に着地させる**。`to!T` は入口の設計としては
  手本だが、実装は互換性維持の中間層が10層以上積もり「事実上理解不能」と公式判定。
  テンプレートの中間層は型システムの中に隠れて蓄積する負債で、利用者から見えない分
  意識的に規律を守る必要がある。
- **エラー処理の優先順位**: (1) 設計でエラーを存在ごと消す（検索失敗は例外でなく
  空を返す等）→ (2) 値で返す（`Nullable`/`SumType`）→ (3) それでも例外なら
  `enforce(value)` の形（検査した値を返すので式に埋め込め、`lazy` 引数でメッセージ
  構築は失敗時のみ）。例外は GC を使い `nothrow`/`@nogc` から呼べないことを忘れない。
- **資源管理の技法が正しくても、型の責務が単一でなければ型として失敗する**。`File` は
  参照カウントも所有権出口も正しいが、ロック・バッファ・整形・プラットフォーム差を
  1つの型に抱えて「理解不能」判定。技法は部品単位で持ち、利用者向けの型は薄い合成で作る。

## レビュー早見表

| 兆候 | 問題 | 処方 |
|---|---|---|
| 公開テンプレート関数に制約がない | 本体奥地の不可解なエラーを利用者が読む | §1: 制約 = 仕様 |
| ジェネリック関数に `@safe pure nothrow @nogc` をベタ書き | 緩い型を不当に拒否 | §1: 推論 + 属性付き unittest |
| 広めにマッチする trait（enum・暗黙変換も通す） | 制約の偽陽性 → 微妙なバグ | §1: 厳密マッチ |
| ポリシー型を無条件にメンバ保持 | ステートレスでもサイズが太る | §1: stateSize + alias |
| `string` をそのまま range として処理 | 自動デコードの属性汚染・性能劣化 | §5: byCodeUnit / byGrapheme を明示 |
| ラッパーごとにコピペされた条件付きメンバ | 条件のズレ | §3: mixin template に一元化 |
| 利用箇所に散った `version` 分岐 | 片方だけ直すバグ | §6: 冒頭 alias 層に集約 |
| CTFE 用と実行時用の別関数 | 二重実装・語彙の分裂 | §4: `if (!__ctfe)` で一本化 |
| 雑多な型を1モジュールに追加 | grab-bag 化 | §7: 1モジュール1主題 |
| 互換性のためのラッパー層の追加 | 隠れたテンプレート負債 | §7: 1〜2段で実装に着地 |
