app: 개밥 먹기 — 일하는 프로그램 하나와 그 마찰 보고

1단계 최소 IO: File(읽기), Args capability. 권한의 출처는 여전히 런타임
하나이고, IO 오류는 Result[a, String]이다 — 런타임이 사용자 정의 enum을
만들 수 없고, 만들 수 있게 하면 런타임이 프로그램의 타입을 알아야 한다.

2단계 std 확장: fold, filter, push, concat, reverse, is_empty,
String.split/trim/starts_with/contains, Int.parse.

3단계 samples/app: 설정 파서 + 리포트 도구, 2모듈 304줄. 검사기를 시험
하려고 쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.

산출물은 프로그램이 아니라 docs/friction.md다. 요약:
- 되돌리기 비싼 결정은 하나도 후회되지 않았다. capability 전달, effect
  명시, 실패를 버릴 수 없음, 소진적 match — 300줄 내내 거추장스럽지
  않았고 소진성은 실제로 실수를 잡았다(Value에 경우 하나 추가하니 고칠
  자리 넷을 정확히 짚었다).
- 불편은 전부 되돌리기 싼 것들이었다. 리스트 n번째 접근이 없어 fold로
  우회(40줄), else if가 없어 3~4단 중첩, String.concat이 2항이라 중첩
  지옥. 304줄 중 70줄쯤이 이 셋 때문에 존재한다.

v0의 질문은 "되돌리기 비싼 결정이 옳은가"였고 답은 그렇다이다.

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:06:08 +09:00
co-authored by Claude Opus 5
parent 5ac899df68
commit 5831af7760
14 changed files with 703 additions and 9 deletions
+22
View File
@@ -60,6 +60,28 @@ m100.cool:15:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Tr
m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up.Tri(_)) m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up.Tri(_))
``` ```
## 실제로 써본 결과
`samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴
프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 304줄.
```
$ coolc run samples/app/main.cool samples/app/example.conf
설정
name = "coollang" (text)
threads = 4 (number)
...
항목 5개, 문제 2개
문제
10행: 이름이 비어 있습니다
11행: = 가 하나여야 합니다: broken = a = b
```
쓰면서 걸린 것들을 [`docs/friction.md`](docs/friction.md)에 남겼다.
요약하면: **되돌리기 비싼 결정은 하나도 후회되지 않았고, 불편은 전부
되돌리기 싼 것들이었다.**
## 빌드 ## 빌드
OCaml 5.x와 dune이 필요하다. OCaml 5.x와 dune이 필요하다.
+3 -3
View File
@@ -4,7 +4,7 @@ let usage =
사용법: 사용법:
coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체) coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체)
coolc iface <file.cool> interface 표면과 해시 출력 coolc iface <file.cool> interface 표면과 해시 출력
coolc run <file.cool> typed IR 인터프리터로 실행 coolc run <file.cool> [인자...] typed IR 인터프리터로 실행
coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅) coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅)
coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅) coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅)
coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면) coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
@@ -76,9 +76,9 @@ let () =
match List.tl argv with match List.tl argv with
| "check" :: files -> check_graph files | "check" :: files -> check_graph files
| [ "iface"; file ] -> dump_iface file | [ "iface"; file ] -> dump_iface file
| [ "run"; file ] -> ( | "run" :: file :: args -> (
let st = Coollang.Session.create ~root:(Filename.dirname file) () in let st = Coollang.Session.create ~root:(Filename.dirname file) () in
match Coollang.Session.run st file with match Coollang.Session.run ~args st file with
| Ok out -> | Ok out ->
print_string out; print_string out;
0 0
+188
View File
@@ -0,0 +1,188 @@
# 개밥 먹기 보고 — samples/app을 쓰면서 걸린 것들
2026-08-30. coollang으로 처음 쓴 "일하는 프로그램" 하나(설정 파서 + 리포트
도구, 2모듈 304줄)에서 실제로 걸린 마찰을 적는다.
v0의 샘플 16개는 전부 검사기를 시험하려고 쓴 것이고, 그래서 "언어가 쓸
만한가"에 대해서는 아무것도 말해주지 않았다. 이 문서가 v0가 남기는 마지막
데이터이자 v1 설계의 첫 입력이다.
기록 원칙: **불편은 증거와 함께 적고, 해법은 제안까지만 한다.** 여기서
바로 고치면 그것은 개밥 먹기가 아니라 기능 추가가 된다.
---
## 잘 된 것부터
**1. 시그니처가 프로그램의 전부를 말한다.**
```cool
pub fn main(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all}
```
이 한 줄을 읽으면 이 프로그램이 할 수 있는 일이 끝난다. 네트워크를 쓸 수
없고, 다른 파일을 쓸 수 없고, 프로세스를 띄울 수 없다 — 문서가 아니라
컴파일러가 보장한다. 300줄을 쓰는 내내 이것이 어색하지 않았다. 오히려
`f.read`를 쓰려고 `File`을 인자에 추가하는 순간이 "이 함수가 권한을 하나
더 갖는다"는 사실을 자각하게 만들었다.
**2. `?`가 기대대로 동작했다.**
```cool
let path = first_arg(a.all())?
let text = f.read(path)?
```
읽기 좋고, 실패 경로가 보이고, 삼켜지지 않는다.
**3. 소진성이 실전에서 값을 했다.**
`Value`에 경우 하나(`List`)를 추가해 봤다:
```
config.cool:54:5: 빠진 경우: List(_)
config.cool:62:5: 빠진 경우: List(_)
config.cool:196:5: 빠진 경우: List(_)
config.cool:204:5: 빠진 경우: List(_)
```
고쳐야 할 자리 넷을 전부, 정확히 짚었다. 이것이 없으면 `show_value`
고치고 `get_int`를 잊는다.
**4. 모듈 경계가 자연스러웠다.** 파싱(`config`)과 출력(`main`)을 나누는 데
마찰이 없었고, `Cfg.Config` 같은 한정 이름이 오히려 읽기 좋았다.
---
## 걸린 것 — 심각한 순서로
### F1. 리스트의 n번째를 꺼낼 방법이 없다 (심각)
인덱싱 연산자를 뺀 결정 자체는 옳다고 본다. 그런데 `std`에 대안이 없다.
"첫 원소"를 꺼내려고 이 코드를 네 번 썼다:
```cool
pub fn first_text(xs: List[String]) -> Option[String] {
List.fold(xs, None, fn(acc, x) {
match acc {
Some(prev) => Some(prev),
None => Some(x),
}
})
}
```
`String.split(line, "=")`의 결과에서 앞의 둘을 꺼내는 데는 보조 struct
`Pick`까지 만들어야 했다 — 순전히 "몇 번째를 보고 있는가"를 나르려고.
**304줄 중 약 40줄이 이 문제 하나 때문에 존재한다.**
> 제안: `List.first`, `List.nth(xs, i) -> Option[a]`, 그리고 `String.split`
> 같은 자리에서 흔한 `List.split_first(xs) -> Option[(a, List[a])]`.
> 튜플이 없으므로 마지막 것은 문법 결정을 동반한다.
### F2. `else if`가 없다 (심각)
`if`가 식이고 `else`는 식 하나를 받으므로, 문법상 `else if`가 가능해야
하는데 파서가 받지 않는다. 그래서 이렇게 된다:
```cool
if raw == "true" {
Flag(true)
} else {
if raw == "false" {
Flag(false)
} else {
match Int.parse(raw) { ... }
}
}
```
세 갈래 분기가 3단 중첩이 된다. `parse_line`은 4단까지 갔다.
**이것은 언어 결정이 아니라 파서의 빈틈으로 보인다.** 확인이 필요하다.
> 제안: `else` 뒤에 블록 대신 `if` 식을 허용한다. 새 개념이 아니라
> 이미 있는 규칙(else는 식을 받는다)의 적용이다.
### F3. `String.concat`이 2항이라 중첩 지옥이 된다 (심각)
리포트 한 줄이 이렇게 생겼다:
```cool
c.print(String.concat(" ", String.concat(e.key,
String.concat(" = ", String.concat(Cfg.show_value(e.value),
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
```
읽을 수 없다. 이 프로그램에서 가장 나쁜 코드이고, 원인은 명확하다.
> 제안 두 가지. (a) `String.join(sep, List[String])` — 작고 안전하다.
> (b) 문자열 보간 `"${key} = ${value}"` — 훨씬 낫지만 문법과 타입 규칙을
> 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다.
> 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤.
### F4. struct의 한 필드만 바꿀 방법이 없다 (중간)
```cool
pub fn add_entry(cfg: Config, e: Entry) -> Config {
Config {
entries: List.push(cfg.entries, e),
problems: cfg.problems, // ← 안 바뀌는데 적어야 한다
}
}
```
필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `take_at`은 세 필드를 매번
전부 나열한다.
> 제안: `Config { ..cfg, entries: x }`. affine 타입에서는 `cfg`가 move되는
> 것이므로 소유권 규칙과 충돌하지 않는다. 오히려 명시적이다.
### F5. fold에 인덱스가 없다 (중간)
줄 번호를 세려고 struct를 하나 더 만들었다:
```cool
pub copyable struct Numbered {
no: Int,
cfg: Config,
}
```
"인덱스가 필요한 fold"는 드문 요구가 아니다.
> 제안: `List.fold_indexed`. 또는 `List.enumerate`가 더 조합적이지만
> 튜플이 없어서 지금은 불가능하다. F1의 튜플 문제와 같은 뿌리다.
### F6. Option/Result에 조작 함수가 하나도 없다 (중간)
`map`, `unwrap_or`, `or_else`가 없어서 전부 `match`로 풀었다. `match`
나쁜 것은 아니지만, 세 줄이면 될 것이 여섯 줄이 된다.
> 제안: `Option.map/unwrap_or`, `Result.map/map_err/unwrap_or`.
> 타입 이름이 함수의 이름공간이라는 규칙이 이미 있으므로 문법 결정은 없다.
### F7. std를 늘릴 때마다 인터프리터를 고쳐야 한다 (구조)
`std/list.cool``filter`를 적으면 `lib/interp.ml`에도 구현을 넣어야
한다. 두 곳이 어긋나면 검사는 통과하고 실행이 죽는다.
이것은 v0의 구조적 한계이고 v1에서 사라진다(std를 coollang으로 구현).
다만 v0 동안은 **std 시그니처와 런타임 구현이 일치하는지 검사하는 테스트**가
있어야 한다. 지금은 없다.
---
## 결론
v1로 넘길 때 **F1, F2, F3이 먼저다.** 셋 다 "언어가 틀렸다"가 아니라
"없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 304줄 중
70줄쯤이 이 셋 때문에 존재한다.
반대로 **되돌리기 비싼 결정들은 하나도 후회되지 않았다.** capability를
인자로 나르는 것, effect를 시그니처에 적는 것, 실패를 버릴 수 없는 것,
소진적 match — 300줄을 쓰는 동안 이 넷이 거추장스러웠던 순간이 없었고,
소진성은 오히려 실수를 잡아줬다.
v0의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
남은 불편은 전부 되돌리기 싼 것들이다.
+8
View File
@@ -366,6 +366,14 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다. 것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다. 결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
■ 개밥 먹기 (2026-08)
samples/app — 설정 파서 + 리포트 도구, 2모듈 304줄. 검사기를 시험하려고
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다
— 리스트 n번째 접근 없음, else if 없음, String.concat 2항.
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
■ 측정 (2026-08, v0 fast path) ■ 측정 (2026-08, v0 fast path)
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현. 100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
cold 전체 검사 245ms cold 전체 검사 245ms
+87 -1
View File
@@ -81,8 +81,46 @@ let bind (env : env) n v : env =
(* 런타임이 제공하는 것 *) (* 런타임이 제공하는 것 *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* 문자열 도우미. 언어에 인덱싱 연산자가 없으므로 이 일은 런타임 몫이다. *)
let find_sub hay needle =
let n = String.length needle and h = String.length hay in
let rec go i =
if i + n > h then None
else if String.sub hay i n = needle then Some i
else go (i + 1)
in
if n = 0 then Some 0 else go 0
let split_on s sep =
if sep = "" then [ s ]
else
let n = String.length sep in
let rec go s acc =
match find_sub s sep with
| None -> List.rev (s :: acc)
| Some i ->
go
(String.sub s (i + n) (String.length s - i - n))
(String.sub s 0 i :: acc)
in
go s []
let out = Buffer.create 1024 let out = Buffer.create 1024
(* 프로그램 인자. 런타임이 들고 있다가 Args capability를 통해서만 준다 —
전역 변수로 아무 데서나 읽을 수 있으면 그것이 ambient authority다. *)
let argv : string list ref = ref []
let read_whole path =
let ic = open_in_bin path in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
(* IO 오류는 문자열로 돌려준다. 런타임이 사용자 정의 enum을 만들 수는 없고,
만들 수 있게 하면 런타임이 프로그램의 타입을 알아야 한다. v0의 선은
여기다 — Result[a, String]. *)
let root_capability name : value option = let root_capability name : value option =
match name with match name with
| "Console" -> | "Console" ->
@@ -96,6 +134,26 @@ let root_capability name : value option =
Buffer.add_char out '\n'; Buffer.add_char out '\n';
VUnit ); VUnit );
] )) ] ))
| "File" ->
Some
(VCap
( "File",
[
( "read",
fun args ->
match args with
| [ VStr path ] -> (
try VEnum ("Result", "Ok", [ VStr (read_whole path) ])
with Sys_error m ->
VEnum ("Result", "Err", [ VStr m ]))
| _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ])
);
] ))
| "Args" ->
Some
(VCap
( "Args",
[ ("all", fun _ -> VList (List.map (fun s -> VStr s) !argv)) ] ))
| "TaskScope" -> | "TaskScope" ->
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이 (* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *) 스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
@@ -106,10 +164,24 @@ let builtin pos name (args : value list) : value =
match (name, args) with match (name, args) with
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b) | "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
| "string.len", [ VStr a ] -> VInt (String.length a) | "string.len", [ VStr a ] -> VInt (String.length a)
| "string.is_empty", [ VStr a ] -> VBool (a = "")
| "string.split", [ VStr s; VStr sep ] -> VList (List.map (fun x -> VStr x) (split_on s sep))
| "string.trim", [ VStr s ] -> VStr (String.trim s)
| "string.starts_with", [ VStr s; VStr p ] ->
VBool (String.length s >= String.length p && String.sub s 0 (String.length p) = p)
| "string.contains", [ VStr s; VStr n ] -> VBool (find_sub s n <> None)
| "int.show", [ VInt n ] -> VStr (string_of_int n) | "int.show", [ VInt n ] -> VStr (string_of_int n)
| "int.abs", [ VInt n ] -> VInt (abs n) | "int.abs", [ VInt n ] -> VInt (abs n)
| "int.parse", [ VStr s ] -> (
match int_of_string_opt (String.trim s) with
| Some n -> VEnum ("Result", "Ok", [ VInt n ])
| None -> VEnum ("Result", "Err", [ VStr (s ^ "은(는) 정수가 아닙니다") ]))
| "bool.show", [ VBool b ] -> VStr (if b then "true" else "false") | "bool.show", [ VBool b ] -> VStr (if b then "true" else "false")
| "list.len", [ VList xs ] -> VInt (List.length xs) | "list.len", [ VList xs ] -> VInt (List.length xs)
| "list.is_empty", [ VList xs ] -> VBool (xs = [])
| "list.push", [ VList xs; x ] -> VList (xs @ [ x ])
| "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys)
| "list.reverse", [ VList xs ] -> VList (List.rev xs)
| _ -> | _ ->
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name) fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
@@ -228,6 +300,19 @@ and apply st pos f args =
match args with match args with
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs) | [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
| _ -> fail pos "list.map은 리스트와 함수를 받습니다") | _ -> fail pos "list.map은 리스트와 함수를 받습니다")
| VBuiltin "list.filter" -> (
match args with
| [ VList xs; f ] ->
VList
(List.filter
(fun x -> match apply st pos f [ x ] with VBool b -> b | _ -> false)
xs)
| _ -> fail pos "list.filter는 리스트와 함수를 받습니다")
| VBuiltin "list.fold" -> (
match args with
| [ VList xs; init; f ] ->
List.fold_left (fun acc x -> apply st pos f [ acc; x ]) init xs
| _ -> fail pos "list.fold는 리스트, 초기값, 함수를 받습니다")
| VBuiltin n -> builtin pos n args | VBuiltin n -> builtin pos n args
| VNative f -> f args | VNative f -> f args
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other)) | other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
@@ -313,10 +398,11 @@ and eval_binary st env op a b pos =
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은 (* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
프로그램 안에 존재하지 않는다. *) 프로그램 안에 존재하지 않는다. *)
let run (prog : Ir.program) (entry : string) let run ?(args = []) (prog : Ir.program) (entry : string)
(main_params : (string * string) list) : (string, Token.pos * string) result (main_params : (string * string) list) : (string, Token.pos * string) result
= =
Buffer.clear out; Buffer.clear out;
argv := args;
let st = { prog } in let st = { prog } in
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
| None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다") | None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다")
+2 -2
View File
@@ -258,7 +258,7 @@ let main_params (m : Ast.modul) =
| _ -> []) | _ -> [])
m.items m.items
let run st path : (string, error) result = let run ?(args = []) st path : (string, error) result =
load st path; load st path;
let errs = errors st in let errs = errors st in
if errs <> [] then Error (List.hd errs) if errs <> [] then Error (List.hd errs)
@@ -275,6 +275,6 @@ let run st path : (string, error) result =
st.modules [] st.modules []
in in
let prog = Ir.of_program mods in let prog = Ir.of_program mods in
match Interp.run prog path (main_params e.ast) with match Interp.run ~args prog path (main_params e.ast) with
| Ok out -> Ok out | Ok out -> Ok out
| Error (pos, msg) -> Error (err_of path pos msg)) | Error (pos, msg) -> Error (err_of path pos msg))
+208
View File
@@ -0,0 +1,208 @@
// 설정 파일 파서.
//
// 이 파일은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다.
// 그것이 이 파일의 목적이다 — 언어가 실제로 쓸 만한지는 이런 코드에서만
// 드러난다.
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
// 설정 값. 타입이 셋뿐이므로 열거형이 맞다.
pub enum Value {
Text(String),
Number(Int),
Flag(Bool),
}
pub copyable struct Entry {
line: Int,
key: String,
value: Value,
}
// 오류에 줄 번호가 있어야 사람이 고칠 수 있다.
pub copyable struct Problem {
line: Int,
message: String,
}
pub copyable struct Config {
entries: List[Entry],
problems: List[Problem],
}
// 값 하나를 읽는다. 따옴표도 타입 표기도 없다 — 모양으로 정한다.
// true/false는 Flag, 정수로 읽히면 Number, 나머지는 Text.
pub fn parse_value(raw: String) -> Value {
if raw == "true" {
Flag(true)
} else {
if raw == "false" {
Flag(false)
} else {
match Int.parse(raw) {
Ok(n) => Number(n),
Err(_) => Text(raw),
}
}
}
}
pub fn show_value(v: Value) -> String {
match v {
Text(s) => String.concat("\"", String.concat(s, "\"")),
Number(n) => Int.show(n),
Flag(b) => if b { "true" } else { "false" },
}
}
pub fn type_name(v: Value) -> String {
match v {
Text(_) => "text",
Number(_) => "number",
Flag(_) => "flag",
}
}
// 한 줄을 읽는다. 빈 줄과 주석은 값이 없고, 그것은 오류가 아니다.
// 그래서 Result가 아니라 Option[Result[...]]가 필요해 보이지만, 그러면
// 부르는 쪽이 두 겹을 벗겨야 한다. Config 하나에 둘 다 모으는 편이 낫다.
pub fn parse_line(cfg: Config, no: Int, raw: String) -> Config {
let line = String.trim(raw)
if String.is_empty(line) {
cfg
} else {
if String.starts_with(line, "#") {
cfg
} else {
let parts = String.split(line, "=")
if List.len(parts) == 2 {
let key = String.trim(head_or(parts, ""))
let val = String.trim(second_or(parts, ""))
if String.is_empty(key) {
add_problem(cfg, no, "이름이 비어 있습니다")
} else {
add_entry(cfg, Entry {
line: no,
key: key,
value: parse_value(val),
})
}
} else {
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
}
}
}
}
pub fn add_entry(cfg: Config, e: Entry) -> Config {
Config {
entries: List.push(cfg.entries, e),
problems: cfg.problems,
}
}
pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config {
Config {
entries: cfg.entries,
problems: List.push(cfg.problems, Problem { line: no, message: msg }),
}
}
// 인덱싱 연산자가 없으므로 앞의 둘을 꺼내는 일은 이름 있는 함수가 한다.
pub fn head_or(xs: List[String], fallback: String) -> String {
List.fold(xs, Pick { taken: false, at: 0, found: fallback }, fn(p, x) {
take_at(p, x, 0)
}).found
}
pub fn second_or(xs: List[String], fallback: String) -> String {
List.fold(xs, Pick { taken: false, at: 0, found: fallback }, fn(p, x) {
take_at(p, x, 1)
}).found
}
// fold로 n번째를 고른다. 셋 다 필요하다 — 몇 번째를 보고 있는지(at),
// 이미 골랐는지(taken), 무엇을 골랐는지(found).
pub copyable struct Pick {
taken: Bool,
at: Int,
found: String,
}
pub fn take_at(p: Pick, x: String, want: Int) -> Pick {
if p.taken {
Pick { taken: true, at: p.at + 1, found: p.found }
} else {
if p.at == want {
Pick { taken: true, at: p.at + 1, found: x }
} else {
Pick { taken: false, at: p.at + 1, found: p.found }
}
}
}
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,
}
// 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다.
pub fn lookup(cfg: Config, key: String) -> Result[Value, String] {
let hit = List.filter(cfg.entries, fn(e) { e.key == key })
match first_entry(hit) {
Some(e) => Ok(e.value),
None => Err(String.concat("설정에 없습니다: ", key)),
}
}
pub fn first_entry(xs: List[Entry]) -> Option[Entry] {
List.fold(xs, None, fn(acc, e) {
match acc {
Some(prev) => Some(prev),
None => Some(e),
}
})
}
// 리스트의 첫 원소. fold로 쓴다 — 언어에 인덱싱이 없다.
pub fn first_text(xs: List[String]) -> Option[String] {
List.fold(xs, None, fn(acc, x) {
match acc {
Some(prev) => Some(prev),
None => Some(x),
}
})
}
pub fn get_int(cfg: Config, key: String) -> Result[Int, String] {
match lookup(cfg, key)? {
Number(n) => Ok(n),
Text(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
Flag(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
}
}
pub fn get_flag(cfg: Config, key: String) -> Result[Bool, String] {
match lookup(cfg, key)? {
Flag(b) => Ok(b),
Text(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
Number(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
}
}
+11
View File
@@ -0,0 +1,11 @@
# 예제 설정
name = coollang
threads = 4
port = 8080
verbose = true
greeting = hello world
# 아래 두 줄은 일부러 틀렸다
= 42
broken = a = b
+96
View File
@@ -0,0 +1,96 @@
// 설정 파일 리포트 도구.
//
// 파일을 읽고, 파싱하고, 문제를 보고하고, 요약을 낸다.
// 권한은 셋뿐이다 — 읽기(File), 인자(Args), 출력(Console). 그 밖의 일은
// 이 프로그램이 할 수 없다. 시그니처에 적힌 것이 이 프로그램의 전부다.
import "config" as Cfg
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
pub capability Console {
fn print(s: String) effects {Console.print}
}
pub capability File {
fn read(path: String) effects {File.read} -> Result[String, String]
}
pub capability Args {
fn all() effects {Args.all} -> List[String]
}
pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} {
c.print("설정")
List.each(cfg.entries, fn(e) {
c.print(String.concat(" ", String.concat(e.key,
String.concat(" = ", String.concat(Cfg.show_value(e.value),
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
})
c.print("")
c.print(String.concat("항목 ", String.concat(Int.show(List.len(cfg.entries)),
String.concat("개, 문제 ", String.concat(Int.show(List.len(cfg.problems)), "개")))))
if List.is_empty(cfg.problems) {
c.print("문제 없음")
} else {
c.print("")
c.print("문제")
List.each(cfg.problems, fn(p) {
c.print(String.concat(" ", String.concat(Int.show(p.line),
String.concat("행: ", p.message))))
})
}
}
// 타입이 있는 조회. 없거나 타입이 다르면 Err이고, 둘 다 사람이 읽을 수 있다.
pub fn check_known(c: Console, cfg: Cfg.Config) effects {Console.print} {
c.print("")
c.print("알려진 설정")
show_int(c, cfg, "threads")
show_int(c, cfg, "port")
show_flag(c, cfg, "verbose")
}
pub fn show_int(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
match Cfg.get_int(cfg, key) {
Ok(n) => c.print(String.concat(" ", String.concat(key,
String.concat(" = ", Int.show(n))))),
Err(m) => c.print(String.concat(" ", m)),
}
}
pub fn show_flag(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
match Cfg.get_flag(cfg, key) {
Ok(b) => c.print(String.concat(" ", String.concat(key,
String.concat(" = ", if b { "true" } else { "false" })))),
Err(m) => c.print(String.concat(" ", m)),
}
}
pub fn first_arg(xs: List[String]) -> Result[String, String] {
match Cfg.first_text(xs) {
Some(p) => Ok(p),
None => Err("설정 파일 경로가 필요합니다"),
}
}
pub fn main(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all} {
match run(c, f, a) {
Ok(_) => unit,
Err(m) => c.print(String.concat("오류: ", m)),
}
}
pub fn run(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all} -> Result[Unit, String] {
let path = first_arg(a.all())?
let text = f.read(path)?
let cfg = Cfg.parse(text)
report(c, cfg)
check_known(c, cfg)
Ok(unit)
}
+3
View File
@@ -2,3 +2,6 @@
pub fn show(n: Int) -> String pub fn show(n: Int) -> String
pub fn abs(n: Int) -> Int pub fn abs(n: Int) -> Int
// 실패할 수 있으므로 Result다. 예외도, 0을 돌려주는 관례도 없다.
pub fn parse(s: String) -> Result[Int, String]
+21 -2
View File
@@ -2,14 +2,22 @@
// //
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다. // 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가 // 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
// 여기서 결정된다. each map이 effect 변수 하나로 표현되지 않으면 규칙이 // each, map, fold, filter에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
// 틀린 것이고, 그건 v1로 미룰 수 없는 발견이다.
// //
// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서 // e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서
// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다. // 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다.
pub fn len[a](xs: List[a]) -> Int pub fn len[a](xs: List[a]) -> Int
pub fn is_empty[a](xs: List[a]) -> Bool
// 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다.
pub fn push[a](xs: List[a], x: a) -> List[a]
pub fn concat[a](xs: List[a], ys: List[a]) -> List[a]
pub fn reverse[a](xs: List[a]) -> List[a]
pub fn each[a, e: effects]( pub fn each[a, e: effects](
xs: List[a], xs: List[a],
f: fn(a) effects e, f: fn(a) effects e,
@@ -19,3 +27,14 @@ pub fn map[a, b, e: effects](
xs: List[a], xs: List[a],
f: fn(a) effects e -> b, f: fn(a) effects e -> b,
) effects e -> List[b] ) effects e -> List[b]
pub fn filter[a, e: effects](
xs: List[a],
keep: fn(a) effects e -> Bool,
) effects e -> List[a]
pub fn fold[a, acc, e: effects](
xs: List[a],
init: acc,
f: fn(acc, a) effects e -> acc,
) effects e -> acc
+11
View File
@@ -1,4 +1,15 @@
// 표준 라이브러리: 문자열. // 표준 라이브러리: 문자열.
//
// 인덱싱 연산자가 언어에 없다. 문자열을 자르는 일은 이름 있는 함수가 한다 —
// 한 개념 한 방식.
pub fn len(s: String) -> Int pub fn len(s: String) -> Int
pub fn is_empty(s: String) -> Bool
pub fn concat(a: String, b: String) -> String pub fn concat(a: String, b: String) -> String
// 구분자로 자른다. 구분자가 없으면 원본 하나짜리 리스트.
pub fn split(s: String, sep: String) -> List[String]
pub fn trim(s: String) -> String
pub fn starts_with(s: String, prefix: String) -> Bool
pub fn contains(s: String, needle: String) -> Bool
+4 -1
View File
@@ -3,4 +3,7 @@
(libraries coollang) (libraries coollang)
(deps (deps
(glob_files %{workspace_root}/samples/*.cool) (glob_files %{workspace_root}/samples/*.cool)
(glob_files %{workspace_root}/std/*.cool))) (glob_files %{workspace_root}/std/*.cool)
(glob_files %{workspace_root}/samples/app/*)
(glob_files %{workspace_root}/samples/modules/*.cool)
(glob_files %{workspace_root}/samples/run/*.cool)))
+39
View File
@@ -1232,3 +1232,42 @@ let () =
in in
List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms
&& List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms) && List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms)
(* ------------------------------------------------------------------ *)
(* 실제 프로그램 *)
(* *)
(* samples/app은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다. *)
(* 언어가 쓸 만한지는 이런 코드에서만 드러난다. *)
(* ------------------------------------------------------------------ *)
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.run ~args:[ "../samples/app/example.conf" ] st
"../samples/app/main.cool"
with
| Error e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
check "app: 설정 리포트가 돈다" false
| Ok out ->
check "app: 항목과 문제를 센다"
(has_sub out "항목 5개, 문제 2개");
check "app: 값의 타입을 모양으로 정한다"
(has_sub out "threads = 4 (number)"
&& has_sub out "verbose = true (flag)"
&& has_sub out "name = \"coollang\" (text)");
check "app: 문제에 줄 번호가 붙는다"
(has_sub out "10행: 이름이 비어 있습니다"
&& has_sub out "11행: = 가 하나여야 합니다");
check "app: 타입 있는 조회" (has_sub out "port = 8080")
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.run st "../samples/app/main.cool" with
| Ok out -> check "app: 인자가 없으면 말해준다" (has_sub out "경로가 필요합니다")
| Error _ -> check "app: 인자가 없으면 말해준다" false
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with
| Ok out -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
| Error _ -> check "app: 없는 파일을 Err로 돌려준다" false