1 Lowent 는 무엇을 하려는 언어인가
이 장의 필요성과 맥락
fn, 인자로 건네받는 권한)이 임의의 취향이 아니라 한 설계의 여러 얼굴로 보인다. 그래서 첫 자리에 목표와 이 책의 읽는 법을 놓는다.이 장이 끝나면
이 장에서 답할 질문
- 머리에 적는 것이 그렇게 많으면 코드가 길어지지 않는가?
- 문법이 그렇게 좁으면 새 기능은 어떻게 들어오는가?
1.1 머리만 읽고 알 수 있는가#
프로그램을 고치는 사람이 가장 많이 하는 일은 남이 쓴 함수를 부르기 전에 그 함수가 무엇을 하는지 알아내는 일이다. 이름은 parse_header 인데 파일을 열지는 않는가? 메모리를 할당하는가? 실패하면 어떻게 알려 주는가? 스레드를 띄우는가? 대부분의 언어에서 이 질문의 답은 본문에 있다. 본문을 읽고, 본문이 부르는 함수의 본문을 읽고, 또 그 아래를 읽는다.
Lowent 는 그 답을 머리로 끌어올린다. 다음은 이 책에서 처음 만나는 op 의 머리다.
proc main input out cap io . output u8 . effects io .낱말을 모르더라도 몇 가지는 읽힌다. 이 op 은 proc 이다(순수하지 않다). out 이라는 이름으로 io 권한을 받는다. 돌려주는 값은 u8 이다. 그리고 io 라는 효과를 낸다. 반대로 적혀 있지 않은 것도 읽힌다 — 이 op 은 메모리를 할당하지 않고(alloc 이 없다), 파일 시스템 권한이 없으며, 스레드를 띄우지 않는다(concurrent 가 없다). 적혀 있지 않으면 할 수 없다. 컴파일러가 그것을 검사한다.
실제로 돌아가는 작은 프로그램으로 보자.
examples/ch01/heads.low
module heads .
rem run: main
rem fn — 같은 입력이면 같은 답. 화면에도 파일에도 닿지 않는다
fn area input w u64 . input h u64 . output u64 .
requires le w 1000 .
requires le h 1000 .
do
return mul w h .
end
rem proc — 바깥에 흔적을 남길 수 있다. 무엇을 하는지(effects io)와 누가 허락했는지(cap io)가 머리에 보인다
proc main input out cap io . output u8 . effects io .
do
let a u64 be area 3 4 .
let n u64 be write_out out 1 "area is twelve\n" .
return narrow u8 a .
end
실행 결과
$ lowentc --run main heads.low
area is twelve
main() = 12
module heads .는 이 파일의 이름표다. 모든 파일이 이 한 줄로 시작한다.rem으로 시작하는 줄은 사람에게 하는 말(주석)이다. 컴파일러는 읽지 않는다.fn area … do … end는 넓이를 셈하는 op 이다.input w u64 .는 “w라는 이름으로 부호 없는 64 비트 정수를 받는다” 이고,output u64 .는 돌려줄 값의 타입이다.requires le w 1000 .는 “w는 1000 이하여야 한다” 는 약속이다. 곱이 넘치지 않게 하려는 것이다.return mul w h .는w곱하기h를 돌려준다. 연산 이름이 앞에 오고 인자가 뒤에 온다. 문장은 떨어진 마침표로 끝난다.main은 프로그램이 시작하는 op 이다.let a u64 be area 3 4 .로 12 를 얻어a에 담고,write_out out 1 "…"로 표준출력(1 번)에 한 줄을 쓴다. 이 줄이 가능한 까닭은 머리에cap io를 받았고effects io를 적었기 때문이다. 돌려준 12 는 운영체제에 건네는 종료 값이 된다.
이제 머리가 거짓말을 하게 해 보자. 순수하다고 적은 fn 이 몰래 화면에 쓰려 한다.
examples/ch01/mistake_hiddenio.low
module mistake_hiddenio .
rem expect: E-EFFECT-CALC
rem ✘ fn 이라고 적고서 몰래 화면에 쓰려 한다
fn area_loud input out cap io . input w u64 . input h u64 . output u64 .
requires le w 1000 .
requires le h 1000 .
do
let n u64 be write_out out 1 "computing\n" .
return mul w h .
end
실행 결과
$ lowentc --check mistake_hiddenio.low
mistake_hiddenio.low:8:1 E-EFFECT-CALC: this fn is declared pure but performs `io` — make it a `proc` with `effects …`, or remove the effect
컴파일러는 실행해 보지도 않고 거절한다. fn 이라는 한 낱말이 “이 op 은 바깥에 닿지 않는다” 는 약속이고, 본문이 그 약속을 어기면 번역이 멈춘다. 그래서 fn 을 부르는 사람은 본문을 열어 보지 않아도 된다. 고치려면 proc area_loud … effects io . 로 적어 사실대로 말하면 된다.
문. 머리에 적는 것이 그렇게 많으면 코드가 길어지지 않는가?
답. 길어진다. 대신 읽는 쪽이 짧아진다. 한 op 을 쓰는 일은 한 번이지만 읽는 일은 그 op 을 부르는 모든 자리에서, 고치는 모든 날에 일어난다. Lowent 는 그 비대칭에 걸었다. 그리고 머리의 절들은 모두 컴파일러가 검사하므로 주석처럼 낡지 않는다.
1.2 다섯 가지 생각#
머리를 믿을 수 있게 만드는 장치는 다섯이다. 이 책의 제2부부터 제8부까지가 이 다섯을 하나씩 편다.
| 생각 | 머리에 적는 것 | 다루는 곳 |
|---|---|---|
| 순수와 비순수를 가른다 | fn 또는 proc | 2장 |
| 약속을 적고 검사받는다 | requires · ensures · errors | 14장 |
| 하는 일과 허락을 짝짓는다 | effects · input … cap … | 15·16장 |
| 수거기 없이 메모리를 지킨다 | region · owned · ref | 18–20장 |
| 두 가지로 실행해 맞대 본다 | (머리가 아니라 도구) | 31장 |
표 1.1 — Lowent 를 이루는 다섯 가지 생각
순수의 표시. fn 은 순수하다 — 같은 입력이면 같은 출력이고, 바깥에 흔적을 남기지 않는다. proc 은 그렇지 않을 수 있다. 둘 중 무엇인지는 언제나 적는다. 순수한 fn 이 출력을 하려 들면 컴파일러가 거절한다.
계약. op 은 호출자에게 요구하는 것(requires)과 돌려줄 때 보장하는 것(ensures)을 적는다. 값이 상수로 정해져 있으면 번역할 때 검사하고, 그렇지 않으면 실행할 때 진입과 반환에서 검사한다. 증명된 계약은 본문의 검사를 지운다 — 계약은 문서이면서 최적화의 근거다.
효과와 권한. op 이 세상에 남기는 자국을 effects 로 선언하고, 그 일을 할 권한을 cap 입력으로 건네받는다. Lowent 에는 주변 권한(ambient authority), 곧 어디서나 몰래 쓸 수 있는 전역 출력이나 전역 할당기가 없다. 권한이 인자 목록에 보이지 않으면 그 op 은 그 일을 못 한다.
수거기 없는 메모리. 쓰레기 수거기(GC) 없이 메모리를 안전하게 쓰려고, 값의 수명을 영역(region)에 묶고, 쓰는 자는 하나·읽는 자는 여럿이라는 차용 규칙을 검사한다. 운영체제가 없는 기계를 위해 자라지 않는 고정 창에서만 할당하는 길도 따로 있다.
두 백엔드의 대조. 컴파일러 lowentc 는 같은 중간 표현을 두 가지로 실행한다. 하나는 스스로 도는 가상 기계(VM)이고, 하나는 C 소스로 내보내 C 컴파일러가 기계어로 만드는 네이티브 코드다. 둘이 다른 답을 내면 컴파일러의 결함이다. 이 책의 예제는 전부 이 대조를 통과했다.
흔한 오해. Lowent 는 모든 것을 정적으로 증명하는 언어다
1.3 한 뜻에 한 표기#
Lowent 의 이름에 들어 있는 엔트로피는 무질서의 척도다. 이 언어는 같은 뜻을 여러 가지로 적을 수 있는 자리를 줄이려 한다. 그래서 몇 가지 선택이 따라 나온다.
- 필드는
field p x로만 읽는다.p.x같은 붙임 점은 없다. - 문장과 절은 홀로 선 마침표
.로 닫고, 블록은 언제나do … end다. - 머리의 절은 정해진 한 차례로만 적는다. 차례가 틀리면 거절되고,
--fmt가 고쳐 준다. - 없앤 낱말(
loop,give,on따위)은 조용히 받아 주지 않고E-VOCAB-REMOVED로 거절한다.
한 뜻에 한 표기이면 사람과 도구가 같은 코드를 같은 모양으로 읽는다. 이 책의 예제가 모두 비슷하게 생긴 것은 저자의 취향이 아니라 언어가 그 모양만 허락하기 때문이다.
문. 문법이 그렇게 좁으면 새 기능은 어떻게 들어오는가?
답. 낱말을 늘리는 대신 이미 있는 틀에 절을 하나 더하는 식으로 들어온다. pipe 는 새 반복 문법이 아니라 do … end 블록 안에 줄마다 한 낱말을 적는 틀이고(24장), 병렬은 새 구문이 아니라 op 머리의 parallel 절이다(27장). 그래서 절의 차례표가 이 언어에서 가장 중요한 표 가운데 하나다(3장).
1.4 이 언어의 처지#
Lowent 는 실험 중인 언어다. 버전은 1 이 되지 않았고, 문법과 표준 라이브러리는 아직 바뀐다. 이 책은 지금 저장소의 컴파일러가 실제로 받아들이는 문법으로 쓰였고, 컴파일러가 바뀌면 책도 따라 바뀐다. 책의 판 번호가 따로 있는 이유다.
컴파일러는 C23 으로 쓰였고 외부 의존은 벤더링한 proven_c_lib 하나다. 네이티브 코드는 C 로 내보내 시스템의 C 컴파일러로 짓는다. 그래서 C 컴파일러가 있는 곳이면 대개 돌아가고, 운영체제가 없는 마이크로컨트롤러도 대상에 들어 있다(30장).
실제 사례. 이 책의 예제는 어떻게 검증되나
.low 파일은 docs/manual/examples/ 에 있고, 파일마다 첫머리에 지시가 한 줄 붙어 있다. rem run: … 이면 VM 과 네이티브로 둘 다 실행해 출력이 같아야 하고, rem expect: E-… 이면 컴파일러가 바로 그 진단으로 거절해야 하며, 지시가 없으면 --check 를 통과해야 한다. 지면의 실행 결과는 사람이 옮겨 적지 않았다 — 검증 스크립트가 남긴 출력을 그대로 싣는다.1.5 이 책을 읽는 법#
이 책은 두 부류의 독자를 함께 생각하며 썼다.
프로그래밍이 처음인 독자는 제1부부터 차례대로 읽는다. 문법 하나마다 “무엇을 하는가” 와 함께 “왜 이렇게 생겼는가” 를 적었고, 예제 뒤에는 그 줄들이 무엇을 하는지 풀이가 따라온다. 제2장부터 제37장까지 장 끝의 “흔한 실수” 는 처음 배우는 사람이 실제로 부딪히는 오류와 그 진단을 그대로 보여 준다. 오류 메시지를 두려워하지 않아도 된다. 이 언어에서 진단은 “틀렸다” 가 아니라 “여기가 약속과 다르다” 는 안내다. 처음 만나는 말은 아래 표로 먼저 익혀 둔다.
다른 언어를 써 본 독자는 예제 코드와 각 장의 “이 장의 문법 한눈에” 표만 따라가도 된다. 예제와 그 표가 그 장의 문법을 함께 보여 준다. C 를 알면 제5부와 제8부가 쉽고, Rust 를 알면 제5부의 차용 규칙이 익숙할 것이다. 익숙한 언어에서 가져온 생각이 여기서 틀리는 자리는 “흔한 오해” 상자에 모았다.
| 말 | 뜻 |
|---|---|
| 소스 · 모듈 | 사람이 쓰는 .low 파일 하나 · 그 파일이 이루는 이름 붙은 단위(21장) |
| 컴파일러 · 번역 | 소스를 읽어 검사하고 실행할 수 있는 모양으로 바꾸는 프로그램(lowentc) · 그 일 |
| 진단 | 컴파일러가 알려 주는 오류(E-)·경고(W-)·알림(N-). 이름이 곧 찾아볼 열쇠다(부록 B) |
| 값 · 타입 | 수나 글자 같은 자료 하나 · 그 값이 어떤 종류이고 몇 비트인지(u64 는 부호 없는 64 비트 정수) |
| 지역 이름 | 값에 붙인 이름. 바꾸지 않으면 let, 바꾸면 var(6장) |
| op | 이름을 붙인 일 한 덩어리. 다른 언어의 함수다. 순수하면 fn, 아니면 proc(5장) |
| 효과 · 권한 | op 이 바깥에 남기는 자국(effects io) · 그 일을 해도 된다는 허락(cap io)(15·16장) |
| 계약 | op 이 받기 전에 요구하고 돌려줄 때 보장하는 조건(requires · ensures)(14장) |
표 1.2 — 처음 만나는 말
각 장은 같은 모양으로 시작한다. 먼저 그 장이 기대는 앞 장을 적고(먼저 알아야 할 것), 그것을 한 번 꺼내 보는 문답을 두고, 이 장이 왜 이 자리에 있는지와 끝나면 무엇을 얻는지를 적은 뒤, 장에서 답할 질문 목록을 보인다. 본문 중간의 장치는 다음과 같다.
| 장치 | 쓰임 |
|---|---|
| 문답 | 읽다가 떠오를 법한 질문과 그 답 |
| 흔한 오해 | 그럴듯하지만 틀린 생각과, 왜 틀렸는지 |
| 실제 사례 | 이 언어나 표준 라이브러리에서 실제로 있었던 일 |
| 수학 상자 | 건너뛰어도 본문이 이어지는 형식적 설명 |
| 시연 | 예제 파일과 검증 스크립트가 남긴 실제 출력 |
표 1.3 — 본문에서 만나는 장치
처음 읽는다면 제1부부터 제4부까지는 차례대로 읽기를 권한다. 그 뒤로는 필요한 부로 건너뛰어도 된다. 표준 라이브러리를 찾는다면 제9부로, 이 언어의 주장이 어디까지 참인지 궁금하다면 제10부로 가면 된다.