Files
coollang/samples/app/main.cool
T
coolguyandClaude Opus 5 5831af7760 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
2026-08-30 16:06:08 +09:00

97 lines
3.1 KiB
Plaintext

// 설정 파일 리포트 도구.
//
// 파일을 읽고, 파싱하고, 문제를 보고하고, 요약을 낸다.
// 권한은 셋뿐이다 — 읽기(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)
}