無駄と文化

実用的ブログ

今週うまくいったドキュメンテーション

ドキュメンテーションは大変だ。知識を絞り出すのと文章を組み立てるのは別の知的作業で、それを同時にやらなければいけない。
私はよどみなく文章を書くのが得意ではないので、大いに生成 AI の力を借りたい。

今週やってみてうまくいった方法を書き留める。

 

目次

 

成功事例①: ざっくり書いて、リライトしてもらう

データ集計での期間の考え方についてガーっと書いた。読みづらいなぁと思いながら書いているので筆が重い。

# 変数による集計期間指定について

集計期間は dbt 変数の `start_date`, `end_date` で指定します。

## `start_date`, `end_date` の意味論

最新日付の集計だけでなく、過去日付を指定してバックフィル集計を実行するために `start_date`, `end_date` は使われます。

※ バックフィル集計: データが入っていない過去日付に対して集計を実行したり、過去日付に対して再集計すること

### 日次集計の場合

日次集計 (毎日集計) を想定したモデル models/summary/arppu_1d.sql に対して `{ "start_date": "2025-02-01", "end_date": "2025-02-03" }` と指定して集計した場合、これは「arppu_1d の日次集計を `2025-02-01`~`2025-02-03` の3日分同時に実行せよ」という意味になります。

### 週次集計・月次集計の場合

週次集計, 月次集計については事情が異なってきます。週次集計を想定したモデル models/summary/arppu_7d.sql に対して `{ "start_date": "2025-02-01", "end_date": "2025-02-03" }` と指定して集計した場合、これは「arppu_7d の週次集計を `2025-02-01`~`2025-02-03` を **含む週に対して** 同時に実行せよ」という意味になります。

したがって、`2025-02-01`~`2025-02-03` は2週間にまたがっていますから、2週分の週次集計が同時に実行されるようにモデルを実装するのが妥当になります。

これを Gemini に渡してリライトを依頼した。

 

dbt でモデルを実装するときのあり方についてドキュメントを書いています。 理解度の高い私が書いたので内容は正確です。しかし不慣れなプロジェクトメンバーには読みづらいかも。 文章を読みやすくするための提案。具体的な文章の提示をおねがいできますか。

そうすると跡形もなく書き換えられてこのように返ってきた。

 

## 修正案:dbt モデルにおける集計期間指定のガイドライン

このドキュメントでは、dbt 変数 `start_date``end_date` を使った集計期間の制御方法と、その考え方を解説します。

---

### 1. `start_date`, `end_date` の役割

これら 2 つの変数は、**「どの日付(期間)の集計結果を作成したいか」**を指定するために使用します。
日々の自動実行だけでなく、過去のデータを遡って集計し直す「バックフィル」の際にもこの変数を利用します。

> **用語解説:バックフィル**
> データの欠損を埋めたり、ロジック変更に伴い過去の日付に遡って再集計を行うこと。

---

### 2. 集計単位ごとの解釈(意味論)

指定した期間が「どのアウトプット(行)」を作成するのか、集計単位によって解釈が異なります。

#### 日次集計(1d モデル)の場合

指定された期間を **1日ずつ** 実行します。

* **例:** `{ "start_date": "2025-02-01", "end_date": "2025-02-03" }`
* **結果:** `02-01`, `02-02`, `02-03` の **3日分** のデータが作成されます。

#### 週次・月次集計(7d / 30d モデル)の場合

指定された期間を **「含む」週や月** をすべて実行します。

* **例:** `{ "start_date": "2025-02-01", "end_date": "2025-02-03" }`
* **ルール:** 週の区切りが日曜〜土曜の場合
* `02-01`(土) は、前週のグループに含まれます。
* `02-03`(月) は、次週のグループに含まれます。


* **結果:** この期間は 2 つの週にまたがるため、**2週分** の集計が実行されます。

読みやすくなった。要点を押さえつつ私よりも断然文章がうまい。

 

モデルごとのリライト能力

いくつかのモデルにリライトを依頼してみた。

  • Claude Code (Opus 4.5): 元となる文章を尊重する、大幅なリライトはしてくれない
  • Gemini 3: 大幅にいい感じにリライトしてくれる
  • ChatGPT 5.2: 大幅にいい感じにリライトしてくれるが、テイストが ChatGPT すぎる

Claude Code はエディタでそのまま動いてくれるから便利だけど、大幅にはリライトしてくれないので物足りなかった。

 

大幅なリライトのうれしい副作用

私の文章テイストを捨て去って大幅にリライトさせるなかで「生成 AI の読み違い」が浮き彫りになるのがうれしい副作用だった。
例えば、

  • 私の文章: 「めぐみのあめだ」
  • リライト: 「キャンディーもらった」

となっていたら「いやそっちの "あめ" じゃないから」となる。

実際にはこんな単純な読み違いは起こらないが、ドメインに由来する微妙な概念の読み違いは発生する。
そんなときには「ここの〇〇という概念は××とは別のもので」と説明した後で「先ほどの概念が明確に伝わるようにドキュメントに反映してください」と指示すると、より読み違いにくい文章にブラッシュアップしてくれる。

生成 AI が読み違えるということは人間も読み違えるということだ。理解しづらい概念や紛らわしい表現を事前にピックアップしてより分かりやすい表現に書き直せるのはうれしい。

 

成功事例②: つれづれに書いて、リライトしてもらう

いろいろなトピックは思いつくものの、文章の体裁にするのが面倒なときがある。そんなとき思いついたことを思いついた順に書いて生成 AI に清書してもらうのがいい。

我々のシステムの集計分析をdbtでやるプロジェクトを進めています。
dbtモデルなどを作ったときのレビューをAIエージェントにさせたい。そのためにレビューガイドラインを作ろうと思います。
私が徒然に「〇〇は××じゃないといけなんだよねー」と書き連ねるので、ある程度まとまったところでレビューガイドラインの体裁にしてもらえますか。

まず「つれづれに書くよ」と宣言する。するとこのように応じてくれる。

まずは、私がガイドラインをまとめる際の**ベースとなる構成案**を作成しました。あなたがこれから書き連ねる「こだわり」や「ルール」を、以下のどのカテゴリに当てはめるかイメージしながらアウトプットしていただければと思います。

---

## dbtレビューガイドラインの構成案(イメージ)

AIエージェントが判定しやすく、人間が理解しやすいように、以下のような章立てで整理する予定です。

1. **プロジェクト構造・モデルの責務**
* どのレイヤー(Staging / Intermediate / Marts)に配置すべきか。
* モデル間の依存関係(Sourceの直接参照禁止など)。

2. **SQLコーディングスタイル**
* CTE(共通テーブル式)の書き方。
* 予約語のケース、インデント、カンマの位置。
* SELECT * の禁止など。

3. **命名規則**
* テーブル名、カラム名、エイリアスの付け方。
* フラグや日付型の接尾辞ルール。

4. **テストとドキュメント**
* 必須のテスト(Unique, Not Nullなど)。
* `description` の記述レベル。

5. **パフォーマンスとベストプラクティス**
* 不要なJOINの回避。
* データ型のリキャストやタイムゾーンの扱い。

よくあるトピックを最初から頭出ししてくれている。優秀。

 

ここから思いつくままにぐだぐだと書いていく。

> CTE(共通テーブル式)の書き方。

まず CTE は積極的につかってほしい。
そんで CTE の名前はアンダースコア `_` から初めてほしい。普通のテーブル名・普通のカラム名と区別できるから。

---

DATE型のカラムについて、タイムゾーンは Azia/Tokyo だな。
いまのところは日本市場向けにビジネスしているため。

---

dbtの `source()``ref()` を必ず使おう。
`some_project.some_dataset.some_table` という形で参照しているモデルを定期集計で動かすべきではない。実装途中・実験中ならありえるかもだけど最終的なレビュー時点まで残っていたらそれは異常。

こんな感じ。

そうするとしっかりしたレビューガイドラインの形で文章が出てくる。
トピックの並び替えやグルーピングも問題なし。言葉尻もちゃんと整えてくれる。

 

ちょっとしたテクニック

文章の全文を毎回渡す

生成 AI に元となるドキュメントを渡して「読みやすくして」など指示すると、元の文章の要点を押さえつつリライトしてくれる。

ところが、そこから質問やディスカッションを何巡かして「それをドキュメントに反映して」と指示すると、元の文章の再現性が低下する。
生成 AI からすると何巡かの会話が挟まったことで、過去に渡された文章を半分忘れてしまっている状態だ。

そこでリライトや追記を要求するときは毎回、最新版の文章を渡すようにする。

ここまでの議論をふまえて注意点を追記して。

```markdown
(ここに文章を全文貼る)
```

そうすると、生成 AI は貼られた文章を元にして追記などしてくれる。

 

いろいろなモデルにレビューを投げてみる

同じ文章を渡してもモデルが異なると着眼点も異なる。ドキュメントを書き終えたら、いろいろなモデルに渡して意見をきいてみるといい。

私が経験したパターンは、

  • Gemini 3: 「こんなトピックも追記してみては?」と提案
  • Claude Code (Opus 4.5): 「ここの日付間違えてませんか?」と指摘
  • GitHub Copilot: 「ここタイプミスでは?」と指摘

こんな感じで各々のモデルが少しずつブラッシュアップしてくれた。
面白いのは生成 AI なら真っ先に見つけそうなタイプミスを、3番手の Copilot しか指摘してくれなかったことだ。

たぶんレビュー依頼するときのプロンプトを工夫すると全ての指摘事項を1つのモデルに出させることもできるんだけど、ちゃんと考えるのはだるいのでたくさん投げてみるのもいいと思う。

 

まとめ

生成 AI をうまく使うことで「自分の文章下手だなー」と自分を呪いながら仕事をせずに済むので良い。

 

 

私からは以上です。

Rust で Grass インタプリタを実装する

書くのも読むのも難しい Grass 言語のインタプリタを書きました。Rust 実装です。

github.com

cargo install --git https://github.com/todays-mitsui/rusty-grass.git でインストールして遊べます。

 

モチベーション

Grass 言語は言語仕様がカッチリしていて、かつ難解でいかにも面白げなのでいつかちゃんと理解していと考えていました。

web.archive.org

しかし、狙った文字列を標準出力に出すのも一筋縄ではいかない言語仕様なのでインタプリタを叩きながら理解するのもなかなか難しい。
ふと思ったのが『これインタプリタの実装を自分でやるほうが分かりよいのでは?』ということでした。

 

参考にしたのは Grass 言語の作者である UENO Katsuhiro さんの 公式ドキュメント と、 id:susisu さんの JavaScript 実装 Grass-JS です。

実際に実装をやってみると Grass コードのパーザや隠れた状態を表現するための構造体をすべて自作することになるので理解が深まりました。実装してよかった。

 

実行方法とサンプルコード

インストールが完了すると grass コマンドが使えるようになります。

$ grass path/to/progfile

とすると path/to/progfile のコードを実行できます。

リポジトリにはサンプルコードを含めています。例えば HelloWorld のコードは rusty-grass/example/helloworld.grass これです。

まとめ

難解で、楽しかった。

 

 

私からは以上です。

Debug トレイトを使いこなして構造体を自由自在に印字する

この記事は Rust Advent Calendar 2025 の15日目の記事です。
大遅刻、すみませんすみません。

 


Rust ではオリジナルの構造体を println!() で印字しようとしても、デフォルトでは印字できません。

// 二次元平面上の点を表現する構造体
struct Point {
    name: String,
    x: f32,
    y: f32,
}

fn main () {
    let a = Point {
        name: String::from("A"),
        x: 1.0,
        y: 2.0,
    };

    println!("{:?}", a);
//                   ^ `Point` cannot be formatted using `{:?}`
//                     because it doesn't implement `Debug`
}

{:?} で印字するには Debug トレイトが必要とのことで、derive 属性をつけて Debug を実装してあげれば印字可能になります。

  // 二次元平面上の点を表現する構造体
+ #[derive(Debug)]
  struct Point {
      name: String,
      x: f32,
      y: f32,
  }

  fn main () {
      let a = Point {
          name: String::from("A"),
          x: 1.0,
          y: 2.0,
      };

      println!("{:?}", a);
+     // => Point { name: "A", x: 1.0, y: 2.0 }
  }

#[derive(Debug)] するだけで実装できるのはお手軽でいいですね。

 

独自の形式で印字する

もう一歩踏み込んで構造体の意味に合わせた独自の形式での印字をやってみましょう。

struct Point { name, x, y } は「x座標, y座標を指定した二次元平面上の点」を表現しています。なので、 Point { name: "A", x: 1.0, y: 2.0 } を印字するときには A (1, 2) としてみるとどうでしょうか。
Debug トレイトは独自に impl することも可能です。やってみましょう。

+ use std::fmt::{Debug, Formatter, Result};
  
  // 二次元平面上の点を表現する構造体
- #[derive(Debug)]
  struct Point {
      name: String,
      x: f32,
      y: f32,
  }
  
+ impl Debug for Point {
+     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+         write!(f, "{} ({}, {})", self.name, self.x, self.y)
+     }
+ }
  
  fn main () {
      let a = Point {
          name: String::from("A"),
          x: 1.0,
          y: 2.0,
      };
      
      println!("{:?}", a);
-     // => Point { name: "A", x: 1.0, y: 2.0 }
+     // => A (1, 2)
  }

impl Debug for PointDebug::fmt メソッドを独自に実装しています。
やっていることは単純で、 write!() を呼び出して self.name, self.x, self.y をお好みの形式に印字しているだけです。

write!(f, "{} ({}, {})", self.name, self.x, self.y)

雰囲気としては、format!("{} ({}, {})", self.name, self.x, self.y) と同じことですね。

 

Debug トレイトは Formatter 構造体と密接に結びついている

ここまで読んで「つまり Debug::fmt メソッドは値を文字列化して返せばいいんだな」と思いませんでしたか?私は思ってました。
しかし println!() の結果をよく見てみると、同じ値に対してインデントが付与される場合があることに気づきます。

let piyo2 = vec!["piyo"; 2];

println!("{:?}", piyo2);
// => ["piyo", "piyo"]

println!("{:#?}", piyo2);
// => [
//     "piyo",
//     "piyo",
// ]

Debug::fmt メソッドの結果がただの文字列であれば、このような整形はできないはずです。
write!() による実装はお手軽ですが、Debug トレイトでできることの全てではありません。

次からもっと凝った実装例を見ていきましょう。

 

List 表示, Set 表示

組み込みの構造体に std::fmt::DebugList というものがあります。これを使うと Vec を format!() したときのような表示をユーザーが手軽に再現できます。

use std::fmt::{Debug, Formatter, Result};

struct MyVec<'a, T> {
    items: &'a [T],
}

impl<'a, T: Debug> Debug for MyVec<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.debug_list()
         .entries(self.items.iter())
         .finish()
    }
}

fn main () {
    let v = MyVec { items: &vec!["hoge", "fuga"] };

    println!("{:?}", v);
    // => ["hoge", "fuga"]

    println!("{:#?}", v);
    // => [
    //     "hoge",
    //     "fuga",
    // ]
}

{:?} なら1行で、{:#?} なら複数行でインデントして印字されていますね。

実装は簡単です。 f.debug_list() が DebugList のインスタンスを返すのでこれを使います。
そのあと .entry(item).entries(items_iter) で要素を与えていくだけです。最後に .finish() すれば Result<(), std::fmt::Error> が得られるので、これをそのまま返します。

 

std::fmt::DebugList の仲間に std::fmt::DebugSet があります。
DebugList は [ ... ] 形式で印字してくれていました。DebugSet は { ... } 形式で値を印字してくれます。

Set 表示をしたいときには f.debug_set() を使います。

use std::fmt::{Debug, Formatter, Result};

struct MySet<'a, T> {
    items: &'a [T],
}

impl<'a, T: Debug> Debug for MySet<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.debug_set()
            .entries(self.items.iter())
            .finish()
    }
}

fn main() {
    let s = MySet { items: &["hoge", "fuga"] };

    println!("{:?}", s);
    // => {"hoge", "fuga"}

    println!("{:#?}", s);
    // => {
    //     "hoge",
    //     "fuga",
    // }
}

 

構造体表示, タプル構造体表示

構造体 struct S { ... } やタプル構造体 struct T ( ... )format!() したときのような表示を再現することもできます。
std::fmt::DebugStruct, std::fmt::DebugTuple を使います。

use std::collections::HashMap;
use std::fmt::{Debug, Formatter, Result};

struct MyStruct<'a, K, V> {
    name: String,
    items: &'a HashMap<K, V>,
}

impl<'a, K: Debug + Ord, V: Debug> Debug for MyStruct<'a, K, V> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut entries: Vec<_> = self.items.iter().collect();
        entries.sort_by_key(|(k, _)| *k);

        let mut dbg = f.debug_struct(&self.name);
        for (ref k, ref v) in entries {
            dbg.field(&format!("{:?}", k), v);
        }
        dbg.finish()
    }
}

struct MyTuple<'a, T> {
    name: String,
    items: &'a [T],
}

impl<'a, T: Debug> Debug for MyTuple<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut dbg = f.debug_tuple(&self.name);
        for item in self.items.iter() {
            dbg.field(item);
        }
        dbg.finish()
    }
}

fn main() {
    let s = MyStruct {
        name: String::from("S"),
        items: &HashMap::from([("hoge", "fuga")]),
    };
    println!("{:?}", s);
    // => S { "hoge": "fuga" }

    let t = MyTuple {
        name: String::from("T"),
        items: &["hoge", "fuga"],
    };
    println!("{:?}", t);
    // => T("hoge", "fuga")
}

構造体表示したい場合には f.debug_struct(name) からの .field(k, v) 、タプル構造体表示したい場合には f.debug_tuple(name) からの .field(v) するだけです。

 

応用例

これらの知識を使ってより高度な応用例を紹介します。

インデックス番号付きの List 表示

use std::fmt::{Debug, Formatter, Result};

struct IndexedList<'a, T>(&'a [T]);

impl<'a, T: Debug> Debug for IndexedList<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut dbg = f.debug_list();
        for (i, item) in self.0.iter().enumerate() {
            dbg.entry(&IndexedItem(&(i, item)));
        }
        dbg.finish()
    }
}

struct IndexedItem<'a, T>(&'a T);

impl<'a, T: Debug> Debug for IndexedItem<'a, (usize, &'a T)> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Debug::fmt(&self.0.0, f)?;
        write!(f, ": ")?;
        Debug::fmt(self.0.1, f)
    }
}

fn main() {
    let v = vec!["hoge", "fuga", "piyo"];

    println!("{:?}", v);
    // => ["hoge", "fuga", "piyo"]

    println!("{:?}", IndexedList(&v));
    // => [0: "hoge", 1: "fuga", 2: "piyo"]

    println!("{:#?}", IndexedList(&v));
    // => [
    //     0: "hoge",
    //     1: "fuga",
    //     2: "piyo",
    // ]
}

いつもの List 表示っぽくもありつつ、インデックス番号を添えてくれるのでどの要素が何番目かわかりやすくなります。

 

末尾要素を省略した List 表示

use std::fmt::{Debug, Formatter, Result};

struct EllipsisList<'a, T>(&'a [T]);

impl<'a, T: Debug> Debug for EllipsisList<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut dbg = f.debug_list();
        let mut iter = self.0.iter();
        // 高々3要素まで表示し、残りがあれば省略表示する
        for _ in 0..3 {
            if let Some(item) = iter.next() {
                dbg.entry(item);
            } else {
                break;
            }
        }
        if iter.next().is_some() {
            dbg.entry(&Ellipsis);
        }
        dbg.finish()
    }
}

struct Ellipsis;

impl Debug for Ellipsis {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "...")
    }
}

fn main() {
    let v = vec![0, 1, 2, 3, 4, 5];
    
    println!("{:?}", v);
    // => [0, 1, 2, 3, 4, 5]

    println!("{:?}", EllipsisList(&v));
    // => [0, 1, 2, ...]

    println!("{:#?}", EllipsisList(&v));
    // => [
    //     0,
    //     1,
    //     2,
    //     ...,
    // ]
}

長い List を全部印字する必要がなく、先頭要素だけに興味がある場合はこのような方法も便利ですね。

ポイントは ... を印字するのに dbg.entry("..."); とするのではなく、ユニット構造体 Ellipsis を定義して dbg.entry(&Ellipsis); としているところです。
&strdyn Debug なので dbg.entry("..."); と書くことは可能ですが、これだと印字結果は "..." となり、余計なダブルクォートがついてしまいます。

 

まとめ

Rust の Debug トレイトは #[derive(Debug)]write!() でお手軽に使うこともできるし、カスタマイズしてこだわった表示にもできるのが魅力的ですね。

 

 

私からは以上です。