createの失敗は「外部世界について何が分かっているか」で分ける
createの失敗をResult<T, E>のErr一つで返すと、失敗した操作の中に、既に起きた副作用が含まれている場合を表せない。APIがタイムアウトしても、クラウド側ではリソースができているかもしれない。そこでCarinaは、create失敗を「APIが何を返したか」ではなく、その時点で外部世界について何が分かっているかで分類している。
CreateFailureKindは4つに分かれる。
| 変種 | 分かっていること | stateの扱い |
|---|---|---|
NoResource | 何も作られていない。リクエストが出ていないか、クラウドが何も作らずに拒否した | 記録しない |
OutcomeUnknown | リクエストはクラウドに届いたかもしれないが、識別子も継続も無い | アドレスと開始時刻だけを保護された行として残す |
IdentifierReported | 識別子は返ったが、リソースの存在は観測できていないかもしれない | 識別子を残す |
InFlight | 再開できる継続(トークン)がある。識別子は有ったり無かったり | 次のapplyで同じ操作に戻れるよう残す |
成功の側も二値ではなく、作成後の読み戻しが不完全だったPartialSuccessがある。
分類が回復手段を決める
この分け方が効くのは、変種ごとに次に取るべき手当てが違うからである。とくにOutcomeUnknownは、stateに「あるかもしれない」行として残り、解消されるまでplanをエラーで止める。解消の経路は二つしかない。
- 実在を確かめてimportする。importでこの行を置き換えることを許す
OutcomeUnknownImportReplacementは、対応するEffect::Importと一緒にしか作れない - 実在しないことを確かめて
state forgetする
どちらも「分からない」を黙って「無い」とも「有る」とも扱わない。黙って捨てるのは、落とすことより悪い
分類を型の進み方で強制する
分類は列挙子を選ぶだけでは守れない。providerが勝手にSuccessを組み立てれば終わりだからだ。CarinaではCreateOutcomeのフィールドがprivateで、成功値を直接作るコードはコンパイルできない(compile_failのdoctestがある)。providerは、executorが渡したCreateAttemptを消費して先へ進めるしかない。
selfを消費するので、同じ試行から二つの結果は作れない。どの経路を通るかを決める「送ったかどうか」は、providerではなくhostが観測する。「送ったかどうか」はproviderの申告ではなくhostが観測する。型で壊れた状態を書けなくする原則そのものはtypestateで壊れた状態を書けなくするにある。
根拠の確度
4変種の定義とコメント、CreateOutcomeKindのSuccess / PartialSuccess、CreateAttemptのメソッド、compile_failのdoctest、OutcomeUnknownImportReplacementの生成関数、CreateOutcomeUnknownAbsentResolution::StateForgetはcarina main 273a2520のソースで確認した。表の「stateの扱い」は型とコメントから読んだもので、executorのstate書き込み経路をすべて追ったわけではない(要確認: IdentifierReportedが次のplanでどう扱われるか)。
ChatGPTが会話の中でissuedを(identifier, sent)と引用していた点はCreateAttempt::issuedと一致する。ただしPollAttempt::issuedはidentifierだけを取るので、同名でも型ごとに引数が違う。