std: F5 해결(List.enumerate), F4·F8은 넣지 않는 것으로 닫음

F5 — "튜플이 없어서 enumerate가 불가능하다"고 적었는데 틀렸다. 제네릭
struct 하나면 된다. fold_indexed보다 이쪽이 낫다: enumerate 하나가 기존
each/map/filter/fold 전부와 조합되고, fold_indexed를 만들면 map_indexed,
each_indexed가 따라와 "한 개념 한 방식"을 깬다.
samples/app에서 Numbered struct가 사라졌다 (244줄 → 232줄).

F2에 이어 두 번째로 관찰자가 틀린 사례다. 마찰 8건 중 2건이 "언어가 못
한다"고 적었다가 확인해 보니 되는 것이었다.

F4 — 넣지 않는다. 전체 나열이 귀찮은 것은 맞지만 그 귀찮음이 값을 한다.
필드를 추가하면 모든 생성 지점이 컴파일 오류를 내고, 컴파일러가 전부
방문하도록 강제한다. ..base는 그것을 없앤다. "오류를 더 빨리 잡는다"가
1번 목표인데 F4는 정확히 그것을 깎는 거래다.

F8 — 넣지 않는다. 진짜 질문은 "미사용 지역 변수를 잡을 것인가"가 아니라
"경고 등급을 만들 것인가"였다. 오류로 넣으면 성가시고, 경고로 넣으면 경고
등급의 첫 입주자가 된다. 경고가 없다는 것은 이 언어의 좋은 성질이고 죽은
지역 변수 하나 때문에 팔 것이 아니다.

둘 다 "보류"가 아니라 "닫음"으로 적는다. 근거를 적어두지 않으면 다음에
같은 논의를 처음부터 다시 한다. 다시 열 조건도 함께 적었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 16:30:56 +09:00
co-authored by Claude Opus 5
parent 5e70712578
commit 6c0d08b6b0
6 changed files with 122 additions and 41 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up
## 실제로 써본 결과 ## 실제로 써본 결과
`samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 `samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴
프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 244줄. 프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 232줄.
``` ```
$ coolc run samples/app/main.cool samples/app/example.conf $ coolc run samples/app/main.cool samples/app/example.conf
+93 -19
View File
@@ -19,15 +19,18 @@ v0의 샘플 16개는 전부 검사기를 시험하려고 쓴 것이고, 그래
| F1 | 리스트의 n번째를 꺼낼 방법이 없다 | **해결**`List.first`, `List.nth` | | F1 | 리스트의 n번째를 꺼낼 방법이 없다 | **해결**`List.first`, `List.nth` |
| F2 | ~~`else if`가 없다~~ | **취소** — 관찰자가 틀렸다 | | F2 | ~~`else if`가 없다~~ | **취소** — 관찰자가 틀렸다 |
| F3 | `String.concat`이 2항이라 중첩 지옥 | **해결**`String.join` | | F3 | `String.concat`이 2항이라 중첩 지옥 | **해결**`String.join` |
| F4 | struct의 한 필드만 바꿀 방법이 없다 | **열림** — 문법 결정, 보류 | | F4 | struct의 한 필드만 바꿀 방법이 없다 | **닫음 — 넣지 않는다** |
| F5 | fold에 인덱스가 없다 | **열림**튜플 필요, 보류 | | F5 | fold에 인덱스가 없다 | **해결**`List.enumerate` |
| F6 | Option/Result에 조작 함수가 없다 | **해결**`std/option`, `std/result` | | F6 | Option/Result에 조작 함수가 없다 | **해결**`std/option`, `std/result` |
| F7 | std와 인터프리터가 어긋날 수 있다 | **해결** — 양방향 테스트 | | F7 | std와 인터프리터가 어긋날 수 있다 | **해결** — 양방향 테스트 |
| F8 | 미사용 지역 변수·파라미터를 안 잡는다 | **열림** — 판단 필요 | | F8 | 미사용 지역 변수·파라미터를 안 잡는다 | **닫음 — 넣지 않는다** |
열린 셋은 전부 의도적으로 남긴 것이다. F4와 F5는 문법을 건드려야 하고, 여덟 항목이 전부 처리됐다. 다섯은 해결, 하나는 취소(관찰자 오류), 둘은
표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기 비싼 축을 움직일 수는 없다. "넣지 않는다"로 닫았다.
F8은 고치는 것이 옳은지 자체가 불분명하다.
**닫은 둘을 "보류"가 아니라 "닫음"으로 적는 이유**: 근거까지 적어두지
않으면 다음에 같은 논의를 처음부터 다시 하게 된다. 마음이 바뀔 조건도
같이 적었다 — 그 조건이 오면 다시 연다.
## 잘 된 것부터 ## 잘 된 것부터
@@ -149,7 +152,7 @@ c.print(String.concat(" ", String.concat(e.key,
> 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다. > 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다.
> 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤. > 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤.
### F4. struct의 한 필드만 바꿀 방법이 없다 (중간) ### F4. struct의 한 필드만 바꿀 방법이 없다 — 닫음. 넣지 않는다
```cool ```cool
pub fn add_entry(cfg: Config, e: Entry) -> Config { pub fn add_entry(cfg: Config, e: Entry) -> Config {
@@ -163,10 +166,29 @@ pub fn add_entry(cfg: Config, e: Entry) -> Config {
필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `take_at`은 세 필드를 매번 필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `take_at`은 세 필드를 매번
전부 나열한다. 전부 나열한다.
> 제안: `Config { ..cfg, entries: x }`. affine 타입에서는 `cfg`가 move되는 > 처음 제안: `Config { ..cfg, entries: x }`. affine 타입에서는 `cfg`가
> 것이므로 소유권 규칙과 충돌하지 않는다. 오히려 명시적이다. > move되는 것이므로 소유권 규칙과 충돌하지 않는다고 적었다.
### F5. fold에 인덱스가 없다 (중간) **결론: 넣지 않는다.** 처음 적을 때 놓친 것이 있다.
전체 나열이 귀찮은 것은 맞다. 그런데 **그 귀찮음이 값을 한다.** `Config`
필드를 하나 추가하면 지금은 모든 생성 지점이 "필드가 빠졌다"고 컴파일
오류를 낸다. 컴파일러가 전부 방문하도록 강제한다.
`..cfg`를 넣으면 그것이 사라진다. 새 필드가 조용히 base의 값을 이어받고,
정말로 손봐야 했던 자리를 지나친다.
**"오류를 더 빨리 잡는다"가 1번 목표인데 F4는 정확히 그것을 깎는 거래다.**
브레비티를 얻고 강제 방문을 잃는다. 그 거래가 옳다는 근거가 지금 없다.
소유권 규칙과 충돌하지 않는다는 처음의 관찰은 맞지만, 충돌하지 않는 것과
넣을 값이 있는 것은 다른 문제다.
> 다시 열 조건: 다음 개밥 먹기에서 필드 5개 이상인 struct의 갱신 함수가
> 여러 개 나오고, **매번의 전체 나열이 실제로 아무것도 잡지 못했다면**
> 그때 넣는다. 반대로 한 번이라도 "필드가 빠졌다"가 진짜 버그를 잡았다면
> 이 항목은 영구히 닫힌다.
### F5. fold에 인덱스가 없다 — 해결 (`List.enumerate`)
줄 번호를 세려고 struct를 하나 더 만들었다: 줄 번호를 세려고 struct를 하나 더 만들었다:
@@ -179,8 +201,41 @@ pub copyable struct Numbered {
"인덱스가 필요한 fold"는 드문 요구가 아니다. "인덱스가 필요한 fold"는 드문 요구가 아니다.
> 제안: `List.fold_indexed`. 또는 `List.enumerate`가 더 조합적이지만 > 처음 제안: `List.fold_indexed`. `List.enumerate`가 더 조합적이지만
> 튜플이 없어서 지금은 불가능하다. F1의 튜플 문제와 같은 뿌리다. > 튜플이 없어서 불가능하다고 적었다.
**"튜플이 없어서 불가능하다"가 틀렸다.** 제네릭 struct 하나면 된다:
```cool
pub copyable struct Indexed[a] {
i: Int,
value: a,
}
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
```
`fold_indexed`보다 이쪽이 낫다. `enumerate` 하나가 기존 `each`/`map`/
`filter`/`fold` 전부와 조합된다. `fold_indexed`를 만들면 `map_indexed`,
`each_indexed`가 따라오고 그것이 "한 개념 한 방식"을 깨는 방향이다.
`samples/app`에서 `Numbered` struct가 사라졌다 (244줄 → 232줄):
```cool
// 전 — 줄 번호를 나르려고 struct를 하나 더 만들었다
let start = Numbered { no: 1, cfg: Config { entries: [], problems: [] } }
List.fold(lines, start, fn(acc, line) {
Numbered { no: acc.no + 1, cfg: parse_line(acc.cfg, acc.no, line) }
}).cfg
// 후
let lines = List.enumerate(String.split(text, "\n"))
List.fold(lines, empty, fn(cfg, l) { parse_line(cfg, l.i + 1, l.value) })
```
이것이 **관찰자가 틀린 두 번째 사례**다(F2에 이어). 둘 다 "언어가 못
한다"고 적었는데 확인해 보니 됐다. 마찰을 적을 때 "정말 막히는가"를
확인하는 절차가 없으면 이런 것이 v1 설계 입력으로 들어간다.
### F6. Option/Result에 조작 함수가 하나도 없다 (중간) ### F6. Option/Result에 조작 함수가 하나도 없다 (중간)
@@ -201,7 +256,7 @@ pub copyable struct Numbered {
--- ---
### F8. 미사용 지역 변수와 파라미터를 안 잡는다 (판단 필요) ### F8. 미사용 지역 변수와 파라미터를 안 잡는다 — 닫음. 넣지 않는다
투어용 예제를 쓰다 발견했다. 이 코드가 아무 말 없이 통과한다: 투어용 예제를 쓰다 발견했다. 이 코드가 아무 말 없이 통과한다:
@@ -225,9 +280,26 @@ pub fn f(used: Int, never_used: Int) -> Int {
처리했다고 컴파일이 막히는 것은 실제로 성가시다. 그 성가심이 잡아주는 처리했다고 컴파일이 막히는 것은 실제로 성가시다. 그 성가심이 잡아주는
버그보다 큰지는 지금 데이터로 알 수 없다. 버그보다 큰지는 지금 데이터로 알 수 없다.
> 제안: 지금은 고치지 않는다. 다음 개밥 먹기에서 **죽은 지역 변수를 **결론: 넣지 않는다.** 그리고 처음에 질문을 잘못 잡았다.
> 실제로 남긴 적이 있는지**를 세어보고 결정한다. 세어볼 근거가 없으면
> 넣지 않는 것이 기능 추가 관문의 기본값이다. 진짜 질문은 "미사용 지역 변수를 잡을 것인가"가 아니라 **"coollang에 경고
등급을 만들 것인가"**다.
지금 이 언어에는 경고가 없다. 모든 진단이 오류이고 빌드를 멈춘다. 미사용
지역 변수를 그 등급에 넣으면 디버깅 중에 한 줄 주석 처리했다고 컴파일이
막힌다 — 실제로 성가시다. 그래서 자연스럽게 "경고로 만들자"가 나오는데,
그것이 **경고 등급의 첫 입주자**가 된다. 경고 등급은 한 번 생기면 자란다.
무시되는 진단이 쌓이는 언어가 된다.
**경고가 없다는 것은 지금 이 언어의 좋은 성질이고, 죽은 지역 변수 하나
때문에 팔 것이 아니다.**
미사용 import와의 불일치는 감수한다. 근거가 다르다 — import는 재검사
범위라는 이 아키텍처의 실제 비용을 만들고, 죽은 지역 변수는 아무 비용도
만들지 않는다. 규칙이 고르지 않아 보이는 것과 근거가 없는 것은 다르다.
> 다시 열 조건: 경고 등급을 다른 이유로 만들게 되는 날. 그때는 이 항목이
> 첫 입주자가 아니라 두 번째가 되므로 비용 계산이 달라진다.
--- ---
@@ -285,14 +357,16 @@ c.print(String.join("", [
**F7도 처리했다.** `Interp.implemented` 목록과 `std/*.cool`의 선언이 **F7도 처리했다.** `Interp.implemented` 목록과 `std/*.cool`의 선언이
서로를 덮는지 테스트가 양방향으로 검사한다. 어긋나면 빌드가 깨진다. 서로를 덮는지 테스트가 양방향으로 검사한다. 어긋나면 빌드가 깨진다.
남은 것: F4(struct 부분 갱신), F5(fold 인덱스), F8(미사용 지역 변수). 남은 것 없음. F5는 `List.enumerate`로 해결했고(244줄 → 232줄), F4와 F8은
앞의 둘은 문법 결정이거나 튜플이 필요하고, F8은 고치는 것이 옳은지 자체가 "넣지 않는다"로 닫았다. 둘 다 다시 열 조건을 함께 적었다.
불분명하다. 셋 다 표본을 더 모은 뒤에 판단한다.
--- ---
## 결론 ## 결론
(아래는 처음 쓸 때의 결론이다. 후속에서 F1·F3·F5·F6·F7이 해결되고 F4·F8이
닫혔으므로 지금은 역사적 기록이다.)
v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라 v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라
"std에 없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 292줄 중 "std에 없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 292줄 중
40줄쯤이 F1 하나 때문에 존재한다. 40줄쯤이 F1 하나 때문에 존재한다.
+6 -4
View File
@@ -367,7 +367,7 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다. 결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
■ 개밥 먹기 (2026-08) ■ 개밥 먹기 (2026-08)
samples/app — 설정 파서 + 리포트 도구, 2모듈 292줄. 검사기를 시험하려고 samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다. 쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect 결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다 명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다
@@ -375,9 +375,11 @@ samples/app — 설정 파서 + 리포트 도구, 2모듈 292줄. 검사기를
항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은 항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은
언어의 성질일 수도, 쓴 사람의 습관일 수도 있다. 언어의 성질일 수도, 쓴 사람의 습관일 수도 있다.
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다. 전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
후속: std 보강(List.first/nth, String.join, Option/Result 함수)으로 292줄이 후속: std 보강(List.first/nth/enumerate, String.join, Option/Result 함수)으로
244줄이 됐다. 문법은 건드리지 않았다 — 표본 하나로 되돌리기 비싼 축을 292줄이 232줄이 됐다. 문법은 하나도 건드리지 않았다 — 걸린 것이 전부 std의
움직일 수는 없다. 빈 곳이었지 문법의 문제가 아니었다는 뜻이고, 그것 자체가 결과다.
마찰 8건 중 2건은 "언어가 못 한다"고 적었다가 확인해 보니 되는 것이었다
(else if, enumerate). 개밥 먹기에는 확인 절차가 함께 있어야 한다.
■ 측정 (2026-08, v0 fast path) ■ 측정 (2026-08, v0 fast path)
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현. 100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
+7
View File
@@ -182,6 +182,7 @@ let implemented =
"list.push"; "list.push";
"list.concat"; "list.concat";
"list.reverse"; "list.reverse";
"list.enumerate";
"list.each"; "list.each";
"list.map"; "list.map";
"list.filter"; "list.filter";
@@ -225,6 +226,12 @@ let builtin pos name (args : value list) : value =
match xs with match xs with
| [] -> VEnum ("Option", "None", []) | [] -> VEnum ("Option", "None", [])
| x :: _ -> VEnum ("Option", "Some", [ x ])) | x :: _ -> VEnum ("Option", "Some", [ x ]))
| "list.enumerate", [ VList xs ] ->
VList
(List.mapi
(fun i x ->
VStruct ("List.Indexed", [ ("i", ref (VInt i)); ("value", ref x) ]))
xs)
| "list.nth", [ VList xs; VInt i ] -> ( | "list.nth", [ VList xs; VInt i ] -> (
match List.nth_opt xs i with match List.nth_opt xs i with
| Some x -> VEnum ("Option", "Some", [ x ]) | Some x -> VEnum ("Option", "Some", [ x ])
+5 -17
View File
@@ -103,23 +103,11 @@ pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config {
} }
pub fn parse(text: String) -> Config { pub fn parse(text: String) -> Config {
let lines = String.split(text, "\n") let lines = List.enumerate(String.split(text, "\n"))
let start = Numbered { let empty = Config { entries: [], problems: [] }
no: 1, List.fold(lines, empty, fn(cfg, l) {
cfg: Config { entries: [], problems: [] }, parse_line(cfg, l.i + 1, l.value)
} })
List.fold(lines, start, fn(acc, line) {
Numbered {
no: acc.no + 1,
cfg: parse_line(acc.cfg, acc.no, line),
}
}).cfg
}
// 줄 번호를 같이 나르기 위한 것. fold에 인덱스가 없으니 직접 센다.
pub copyable struct Numbered {
no: Int,
cfg: Config,
} }
// 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다. // 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다.
+10
View File
@@ -18,6 +18,16 @@ pub fn first[a](xs: List[a]) -> Option[a]
pub fn nth[a](xs: List[a], i: Int) -> Option[a] pub fn nth[a](xs: List[a], i: Int) -> Option[a]
// 번호를 붙인 값. fold에 인덱스를 넣는 대신 이것 하나를 둔다 —
// fold_indexed를 만들면 map_indexed, each_indexed가 따라오고 그것이
// "한 개념 한 방식"을 깨는 방향이다. enumerate는 기존 각 함수와 조합된다.
pub copyable struct Indexed[a] {
i: Int,
value: a,
}
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
// 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다. // 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다.
pub fn push[a](xs: List[a], x: a) -> List[a] pub fn push[a](xs: List[a], x: a) -> List[a]