From 6c0d08b6b0285b5b0a2caf9e7cd5760f5c2e82c4 Mon Sep 17 00:00:00 2001 From: coolguy Date: Sun, 30 Aug 2026 16:30:56 +0900 Subject: [PATCH] =?UTF-8?q?std:=20F5=20=ED=95=B4=EA=B2=B0(List.enumerate),?= =?UTF-8?q?=20F4=C2=B7F8=EC=9D=80=20=EB=84=A3=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20=EB=8B=AB=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E --- README.md | 2 +- docs/friction.md | 112 +++++++++++++++++++++++++++++++++------- docs/thesis.md | 10 ++-- lib/interp.ml | 7 +++ samples/app/config.cool | 22 ++------ std/list.cool | 10 ++++ 6 files changed, 122 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 481dd54..274bec6 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up ## 실제로 써본 결과 `samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 -프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 244줄. +프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 232줄. ``` $ coolc run samples/app/main.cool samples/app/example.conf diff --git a/docs/friction.md b/docs/friction.md index 66bf803..0ec78a1 100644 --- a/docs/friction.md +++ b/docs/friction.md @@ -19,15 +19,18 @@ v0의 샘플 16개는 전부 검사기를 시험하려고 쓴 것이고, 그래 | F1 | 리스트의 n번째를 꺼낼 방법이 없다 | **해결** — `List.first`, `List.nth` | | F2 | ~~`else if`가 없다~~ | **취소** — 관찰자가 틀렸다 | | F3 | `String.concat`이 2항이라 중첩 지옥 | **해결** — `String.join` | -| F4 | struct의 한 필드만 바꿀 방법이 없다 | **열림** — 문법 결정, 보류 | -| F5 | fold에 인덱스가 없다 | **열림** — 튜플 필요, 보류 | +| F4 | struct의 한 필드만 바꿀 방법이 없다 | **닫음 — 넣지 않는다** | +| F5 | fold에 인덱스가 없다 | **해결** — `List.enumerate` | | F6 | Option/Result에 조작 함수가 없다 | **해결** — `std/option`, `std/result` | | F7 | std와 인터프리터가 어긋날 수 있다 | **해결** — 양방향 테스트 | -| F8 | 미사용 지역 변수·파라미터를 안 잡는다 | **열림** — 판단 필요 | +| F8 | 미사용 지역 변수·파라미터를 안 잡는다 | **닫음 — 넣지 않는다** | -열린 셋은 전부 의도적으로 남긴 것이다. F4와 F5는 문법을 건드려야 하고, -표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기 비싼 축을 움직일 수는 없다. -F8은 고치는 것이 옳은지 자체가 불분명하다. +여덟 항목이 전부 처리됐다. 다섯은 해결, 하나는 취소(관찰자 오류), 둘은 +"넣지 않는다"로 닫았다. + +**닫은 둘을 "보류"가 아니라 "닫음"으로 적는 이유**: 근거까지 적어두지 +않으면 다음에 같은 논의를 처음부터 다시 하게 된다. 마음이 바뀔 조건도 +같이 적었다 — 그 조건이 오면 다시 연다. ## 잘 된 것부터 @@ -149,7 +152,7 @@ c.print(String.concat(" ", String.concat(e.key, > 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다. > 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤. -### F4. struct의 한 필드만 바꿀 방법이 없다 (중간) +### F4. struct의 한 필드만 바꿀 방법이 없다 — 닫음. 넣지 않는다 ```cool pub fn add_entry(cfg: Config, e: Entry) -> Config { @@ -163,10 +166,29 @@ pub fn add_entry(cfg: Config, e: Entry) -> Config { 필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `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를 하나 더 만들었다: @@ -179,8 +201,41 @@ pub copyable struct Numbered { "인덱스가 필요한 fold"는 드문 요구가 아니다. -> 제안: `List.fold_indexed`. 또는 `List.enumerate`가 더 조합적이지만 -> 튜플이 없어서 지금은 불가능하다. F1의 튜플 문제와 같은 뿌리다. +> 처음 제안: `List.fold_indexed`. `List.enumerate`가 더 조합적이지만 +> 튜플이 없어서 불가능하다고 적었다. + +**"튜플이 없어서 불가능하다"가 틀렸다.** 제네릭 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에 조작 함수가 하나도 없다 (중간) @@ -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`의 선언이 서로를 덮는지 테스트가 양방향으로 검사한다. 어긋나면 빌드가 깨진다. -남은 것: F4(struct 부분 갱신), F5(fold 인덱스), F8(미사용 지역 변수). -앞의 둘은 문법 결정이거나 튜플이 필요하고, F8은 고치는 것이 옳은지 자체가 -불분명하다. 셋 다 표본을 더 모은 뒤에 판단한다. +남은 것 없음. F5는 `List.enumerate`로 해결했고(244줄 → 232줄), F4와 F8은 +"넣지 않는다"로 닫았다. 둘 다 다시 열 조건을 함께 적었다. --- ## 결론 +(아래는 처음 쓸 때의 결론이다. 후속에서 F1·F3·F5·F6·F7이 해결되고 F4·F8이 +닫혔으므로 지금은 역사적 기록이다.) + v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라 "std에 없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 292줄 중 40줄쯤이 F1 하나 때문에 존재한다. diff --git a/docs/thesis.md b/docs/thesis.md index 1cbb1b1..0b353eb 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -367,7 +367,7 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌 결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다. ■ 개밥 먹기 (2026-08) -samples/app — 설정 파서 + 리포트 도구, 2모듈 292줄. 검사기를 시험하려고 +samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다. 결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect 명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다 @@ -375,9 +375,11 @@ samples/app — 설정 파서 + 리포트 도구, 2모듈 292줄. 검사기를 항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은 언어의 성질일 수도, 쓴 사람의 습관일 수도 있다. 전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다. -후속: std 보강(List.first/nth, String.join, Option/Result 함수)으로 292줄이 -244줄이 됐다. 문법은 건드리지 않았다 — 표본 하나로 되돌리기 비싼 축을 -움직일 수는 없다. +후속: std 보강(List.first/nth/enumerate, String.join, Option/Result 함수)으로 +292줄이 232줄이 됐다. 문법은 하나도 건드리지 않았다 — 걸린 것이 전부 std의 +빈 곳이었지 문법의 문제가 아니었다는 뜻이고, 그것 자체가 결과다. +마찰 8건 중 2건은 "언어가 못 한다"고 적었다가 확인해 보니 되는 것이었다 +(else if, enumerate). 개밥 먹기에는 확인 절차가 함께 있어야 한다. ■ 측정 (2026-08, v0 fast path) 100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현. diff --git a/lib/interp.ml b/lib/interp.ml index 7e254a7..3692027 100644 --- a/lib/interp.ml +++ b/lib/interp.ml @@ -182,6 +182,7 @@ let implemented = "list.push"; "list.concat"; "list.reverse"; + "list.enumerate"; "list.each"; "list.map"; "list.filter"; @@ -225,6 +226,12 @@ let builtin pos name (args : value list) : value = match xs with | [] -> VEnum ("Option", "None", []) | 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 ] -> ( match List.nth_opt xs i with | Some x -> VEnum ("Option", "Some", [ x ]) diff --git a/samples/app/config.cool b/samples/app/config.cool index 3c42609..75aa798 100644 --- a/samples/app/config.cool +++ b/samples/app/config.cool @@ -103,23 +103,11 @@ pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config { } pub fn parse(text: String) -> Config { - let lines = String.split(text, "\n") - 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 -} - -// 줄 번호를 같이 나르기 위한 것. fold에 인덱스가 없으니 직접 센다. -pub copyable struct Numbered { - no: Int, - cfg: Config, + let lines = List.enumerate(String.split(text, "\n")) + let empty = Config { entries: [], problems: [] } + List.fold(lines, empty, fn(cfg, l) { + parse_line(cfg, l.i + 1, l.value) + }) } // 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다. diff --git a/std/list.cool b/std/list.cool index ba2b5a1..2e40842 100644 --- a/std/list.cool +++ b/std/list.cool @@ -18,6 +18,16 @@ pub fn first[a](xs: List[a]) -> 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]