想定外の状態は型で表現できなくする
バグが発生する原因のひとつに「想定外の状態」がある。
- a. 存在しない値の参照 (e.g. NullPointerException)
- b. 想定外の状態遷移
- c. 処理し忘れた分岐
これらを型で表現することでエージェントがコンパイラで検証できれば、安全なコードを書く確率が高くなる。具体的には
- a. non-null にする
- b. sealed / discriminated union で想定している状態を明示する
- c. Result/Either とパターンマッチングで網羅性チェックする(期待されるエラーは値で返して網羅的に扱う)
を意識して型を定義する。最近のエージェントは賢いので、コンパイルを通すだけのコードやなくて意図を汲み取ったコードを書いてくれる。
ドメインモデルを設計する
Twitter のようなアプリケーションをつくるとして、バックエンドを TypeScript で書いたらこんなイメージ。
// Branded Types
type UserId = string & { readonly brand: unique symbol };
type TweetId = string & { readonly brand: unique symbol };
// タイムラインのアイテム種別をdiscriminated unionで表現してnullを防ぐ
type TimelineItem =
| { type: "tweet"; tweet: TweetContent }
| { type: "retweet"; retweetedBy: User; tweet: TweetContent }
| { type: "ad"; ad: AdContent };
// 次のカーソルがnullableなんはしゃーない
type Timeline = {
items: TimelineItem[];
nextCursor: string | null;
};
同じ考え方をアプリ側にも持ち込む。Kotlin なら sealed class で UI の状態を定義する。
// ホーム画面の状態
sealed class HomeUiState {
data object Loading : HomeUiState()
data class Success(val timeline: List<TimelineItem>) : HomeUiState()
data class Error(val error: HomeError) : HomeUiState()
}
// タイムラインのアイテム種別
// nullableなプロパティがなくなる
sealed class TimelineItem {
data class Tweet(val tweet: TweetContent) : TimelineItem()
data class Retweet(val retweetedBy: User, val tweet: TweetContent) : TimelineItem()
data class Ad(val ad: AdContent) : TimelineItem()
}
Compose の UiState を Sealed Interface にする話(表示に使ってよいのは UiState だけ)は、これを UI 層でやっているのと同じことになる。エージェントを使うようになる前から効く設計だったと言える。
なぜこれをエージェントに対してやるのかは 型はレビューで伝えていた知識を運ぶ に書いた。