Files
coollang/dogfoods/atomic-update/careful.cool
T
coolguyandClaude Opus 5 410363b230 dogfoods: 2번 Atomic File Updater — ?를 자원과 함께 쓸 수 없다
프로그램을 쓰되 돌리지 않는 방식을 시작한다. std가 원래 선언-전용이므로
실행만 빼고 전부 진짜로 검사된다 — 종이 스케치가 아니라 컴파일러가 검증한
설계다.

합격 기준을 둘로 잡았다: check exit 0 + 모듈이 실제로 해소될 것. 후자가
없으면 전자가 공허한데, 그것을 첫 시도에서 겪었다.

D1 — 상대 경로 import가 패키지로 오인됐다. is_package가 첫 세그먼트에 점이
있는지만 봐서 ".."이 걸렸다. 무서운 것은 버그가 아니라 결과였다: import가
해소되지 않으면 그 모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고
말하지 않는다"는 원칙에 따라 무엇이든 통과한다. 첫 check가 exit 0이었는데
없는 메서드를 불러도 통과하는 상태였다.

D2 — 값 있는 식을 문으로 버릴 수 있었다. fs.remove(path)를 문으로 쓰면
Result가 조용히 사라졌다. 즉 실패를 버리는 방법이 있었고, 내가 개밥 먹기 1차
보고서와 투어에 "이 언어에는 실패를 버릴 방법이 없다"고 적은 것은 틀렸다 —
List.each 하나의 좁은 사실을 언어 전체로 일반화했다. 이제 오류이고, 일부러
버리려면 let _ = 로 적는다. 부산물로 정리 경로의 관용구가 생겼다.

D3(본체) — ?를 자원과 함께 쓸 수 없다. naive.cool 18줄은 조기 반환으로 핸들을
누수하는데 통과한다(v0 정책). 제대로 정리한 careful.cool은 58줄이고 5단 중첩
match이며 ?를 한 번도 못 쓴다. 3.2배다. resource/with가 필요한 이유가 여기
숫자로 있다.

열어둔 것: 함수 타입에 own이 없어 고차 경계에서 소유권이 뚫린다(D5).
Mini Shell과 DB Pool이 정면으로 걸리므로 그 둘 전에 결정해야 한다.
문자 접근이 없어 어휘 분석을 못 쓴다(D6).

소유권 검사가 잡는 것은 확인했다: 두 번 닫기, 닫은 뒤 쓰기, 빌린 핸들 반환.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 18:56:05 +09:00

59 lines
2.0 KiB
Plaintext

// 제대로 정리하는 버전.
//
// naive.cool과 같은 일을 하지만 모든 실패 경로에서 핸들을 닫고 임시 파일을
// 지운다. 두 파일의 차이가 이 개밥 먹기의 산출물이다.
import "../std-draft/fs" as Fs
import "cool.dev/std/string" as String
pub fn update(fs: Fs.Fs, path: String, contents: String)
effects {Fs.create, Fs.write, Fs.sync, Fs.close, Fs.rename, Fs.remove}
-> Result[Unit, Fs.IoError] {
let tmp = String.concat(path, ".tmp")
match fs.create(tmp) {
Err(e) => Err(e),
Ok(f) => finish(fs, f, tmp, path, contents),
}
}
// 핸들을 얻은 뒤. 여기서부터 모든 실패 경로가 f를 닫고 tmp를 지워야 한다.
// ? 를 쓸 수 없다 — 조기 반환이 정리를 건너뛰기 때문이다.
pub fn finish(
fs: Fs.Fs,
own f: Fs.WriteFile,
tmp: String,
path: String,
contents: String,
) effects {Fs.write, Fs.sync, Fs.close, Fs.rename, Fs.remove}
-> Result[Unit, Fs.IoError] {
match fs.write(f, contents) {
Err(e) => abandon(fs, f, tmp, e),
Ok(_) => match fs.sync(f) {
Err(e) => abandon(fs, f, tmp, e),
Ok(_) => match fs.close(f) {
Err(e) => discard(fs, tmp, e),
Ok(_) => match fs.rename(tmp, path) {
Err(e) => discard(fs, tmp, e),
Ok(_) => Ok(unit),
},
},
},
}
}
// 핸들을 아직 들고 있는 실패. 닫고 지운다.
// 정리 중의 실패는 일부러 버린다 — 원래 오류가 더 중요하다.
// let _ = 가 버린다는 사실을 코드에 보이게 한다.
pub fn abandon(fs: Fs.Fs, own f: Fs.WriteFile, tmp: String, e: Fs.IoError)
effects {Fs.close, Fs.remove} -> Result[Unit, Fs.IoError] {
let _ = fs.close(f)
discard(fs, tmp, e)
}
// 핸들은 이미 없다. 임시 파일만 지운다.
pub fn discard(fs: Fs.Fs, tmp: String, e: Fs.IoError)
effects {Fs.remove} -> Result[Unit, Fs.IoError] {
let _ = fs.remove(tmp)
Err(e)
}