理解した前提をコードに戻す

· Updated:

前回、AIと一緒に書いたコードを読み直して、別々の場所にあるコードが、実はひとつの設計になっていた、という話を書いた。

題材にしたのは、2つのイベントが近い時間に起きているかを調べる処理だった。両方のイベントをあらかじめソートしておくことで、2つの列を前から順番に見るだけで判定できる。 コードを読んでいて面白かったのは、「ソートする処理」と「ソート済みであることを前提にした処理」が、別の場所にあったことだった。

nanos.sort_unstable();

この一行があるから、その後の count_matched では一度通り過ぎたところに戻らなくていい。 そこまで理解して、ひとつ気になった。 この「ソート済みである」という前提は、コードのどこに書いてあるんだろう。

count_matched にはコメントがあった。

/// Both slices must be sorted ascending.
pub fn count_matched(events: &[i64], others: &[i64], window: i64) -> usize {
    // ...
}

ちゃんと書いてある。でも、関数のシグネチャだけを見ると、普通の &[i64] なので、ソートされていない値を渡すこともできる。 同じようなところが、別のツールにもあった。

pub fn percentile(sorted: &[f64], p: f64) -> f64 {
    debug_assert!(!sorted.is_empty(), "percentile of empty slice");
    // ...
}

こちらは引数名が sorted になっている。コメントにもソート済みであることが書かれていた。でも、それをコードでは確認していなかった。 しかも percentile は、空の配列については debug_assert! で確認している。 空ならその後かなりわかりやすく壊れるのに、ソートされていない場合は、それっぽい間違った値を返してしまう可能性がある。考えてみると、確認したいのはむしろこっちかもしれない。 じゃあ、どうするか。 最初に考えたのは、型にすることだった。 たとえば Sorted<T> のような型を作って、生成時にソートし、ソート済みの値しか count_matched や percentile に渡せないようにする。そうすれば未ソートの値はそもそも渡せなくなるので、関数側は前提を信頼できる。 でも、少し考えてやめた。 今のコードでは、ソート済みのデータが、多くの場所を経由して持ち回されるわけではない。ソートして、すぐ使う。こういう前提を持つ関数もまだ少ない。 ここで Sorted<T> を作ると、型の定義だけでは終わらない。どう作るか、どう借用するか、i64 と f64 のソートをどう扱うか、と考えることが増えていく。もちろん、それが必要になることもあると思う。でも、今はそこまでではなさそうだった。 結局、追加したのはこれだった。

debug_assert!(events.is_sorted(), "events must be sorted ascending");
debug_assert!(others.is_sorted(), "others must be sorted ascending");

percentile にも追加した。

debug_assert!(
    sorted.is_sorted(),
    "percentile requires an ascending-sorted slice"
);

追加したassertは3つ。 debug_assert! なので、debugビルドでは前提が破られたら気づける。一方、デフォルトのreleaseビルドではチェックされないので、is_sorted() のために毎回配列を走査することもない。 型ほど強い保証ではない。でも、今のコードにはこれくらいでよさそうだった。 さらに「このassert自体のテストも書くべきか」と考えた。でも、それもやめた。debug_assert! と is_sorted() の動作を確認するだけのテストになりそうだったし、assertのためにテストコードのほうが大きくなるのも少し違う気がした。

前回は、AIが書いたコードの「なぜ」を追いかけて、ソートとTwo Pointersのつながりを理解した。今回は、その理解したことを、どこまでコードに戻すかを考えた。 コメントだけでも伝わる。assertにすれば実行時にも確認できる。型にすれば、もっと強く守ることもできる。 強くできるなら、強くしたほうがいいようにも思える。でも、型にできることと、型にすべきことは別なんだと思う。 必要になったら、そのとき型にすればいい。 今回はassertを3つ追加して止めた。

AIとコードを書いていると、生成されたコードを理解することに目が向きがちだけど、そのあとに「理解したことをコードにどう戻すか」を考えるのも面白い。 そして、戻しすぎないことも、たぶん設計のひとつなんだと思う。

← Back to posts