5 op — fn 과 proc
먼저 알아야 할 것
돌아보기
3장의 절 차례표에서 output 과 effects 는 각각 무엇의 뒤에 오는가? 왜 그 자리인가?
답. output 은 데이터 입력 뒤에 온다. 출력 타입이 입력의 타입 매개변수를 쓸 수 있기 때문이다. effects 는 output 뒤, 계약 앞에 온다. 앞에서 받은 권한으로 무엇을 하는지를 적는 자리이기 때문이다. 이 장은 그 머리를 가진 실행 단위 — op — 가 두 갈래로 나뉘는 까닭을 다룬다.
이 장의 필요성과 맥락
fn 이거나 proc 이다. 이 갈래는 제4부의 효과 체계, 제6부의 트레이트, 제7부의 병렬이 모두 기대는 바닥이다. 순수한 op 은 결과를 기억해 두어도, 차례를 바꾸어도, 나누어 돌려도 되기 때문이다. 그래서 수를 다룬 바로 다음, 지역과 흐름보다 먼저 op 의 두 갈래를 세운다.이 장이 끝나면
fn 과 proc 의 차이, 그리고 순수함이 관측적이라는 뜻(안에 갇힌 변이는 순수)을 알게 된다. 매개변수를 input 절로 받고 output 으로 돌려주는 법, 재귀, neg 가 유일한 단항 산술이라는 것을 익힌다. 순수한 op 이 비순수한 op 을 부를 때와 mut 매개변수로 호출자에게 쓸 때 무엇이 거절되는지도 보게 된다.이 장에서 답할 질문
proc인데 실제로 아무 효과도 내지 않으면 어떻게 되는가?
5.1 두 갈래#
op(operation)은 이름과 계약을 가진 실행 단위다. 표면에서는 언제나 두 낱말 가운데 하나로 연다.
fn— 순수한 op. 같은 입력이면 같은 결과를 내고 바깥에 흔적을 남기지 않는다.proc— 효과를 낼 수 있는 op. 무슨 효과를 내는지effects절에 적는다.
기본값은 없다. fn 인지 proc 인지는 이름 앞에 늘 적혀 있다. add 나 len 같은 기본 연산도 op 이다 — 언어가 미리 만들어 둔 op 이라서 기본 연산 이라 부른다.
examples/ch05/kinds.low
module kinds .
rem run: add3 1 2 3
rem run: running_total [4,5,6]
rem run: fact 10
fn add3 input a i32 . input b i32 . input c i32 . output i32 .
do
return add add a. b. . c. . .
end .
rem 지역 가변 상태는 op 안에 갇혀 있으므로 fn 도 쓸 수 있다
fn running_total input xs slice u8 . output u64 .
do
var total u64 0 .
for x xs. do
set total. add total. widen u64 x. . . .
end .
return total. .
end .
fn fact input n u64 . output u64 .
requires le n. 20 . .
do
if le n. 1 . do return 1 . end .
return mul n. fact sub n. 1 . . . .
end .
실행 결과
$ lowentc --run add3 kinds.low 1 2 3
add3(1, 2, 3) = 6
$ lowentc --run running_total kinds.low [4,5,6]
running_total([4,5,6]) = 15
arg0 (written) = [4,5,6]
$ lowentc --run fact kinds.low 10
fact(10) = 3628800
세 op 이 모두 fn 이다. add3 은 인자 셋을 받아 더하고, fact 는 자기 자신을 부르는 재귀다. fact 의 requires le n. 20 . . 은 21! 이 u64 를 넘기 때문에 붙인 계약이다. 이 계약이 없으면 fact 21 은 곱셈에서 멈춘다(4장). 계약이 있으면 진입에서 멈추고, 멈춘 자리가 부르는 쪽의 잘못임을 알린다.
5.2 순수함은 관측으로 정한다#
running_total 을 다시 보자. 본문에 var total 이 있고 set 으로 값을 바꾼다. 그런데도 fn 이다. Lowent 의 순수함은 관측적이기 때문이다. op 안에 갇힌 지역 변수의 변이는 바깥에서 볼 수 없으므로 순수로 친다. 기계어 수준에서 메모리에 쓰는 일이 일어나도, 호출자가 관측할 수 없으면 효과가 아니다.
반대로 호출자가 볼 수 있는 쓰기는 효과다. mut 매개변수를 통해 쓰는 fn 은 거절된다.
examples/ch05/mutparam.low
module mutparam .
rem expect: E-EFFECT-PURITY
fn zero_first input xs mut slice u64 . output u64 .
do
set idx xs. 0 . 0 .
return 0 .
end .
실행 결과
$ lowentc --check mutparam.low
mutparam.low:4:0 E-EFFECT-PURITY: a `fn` WRITES through a `mut` parameter — that write is visible to the CALLER. A fn is an enforced purity contract (SPEC-003 §27): callers may memoise it, reorder it, or elide it. An op that changes caller-owned storage can do none of those. Declare it a `proc`. (Local mutation stays pure: storage confined to the op is not observable — a machine write is not an observable effect. RFC-0057)
진단이 설명하는 대로, fn 은 강제되는 순수 계약이다. 호출자는 fn 의 결과를 기억해 두거나(memoise), 부르는 차례를 바꾸거나, 결과를 쓰지 않으면 호출을 지워도 된다. 호출자의 저장소를 바꾸는 op 에는 그 어느 것도 할 수 없다. 그래서 proc 이어야 한다.
examples/ch05/mutproc.low
module mutproc .
rem run: zero_first [7,8,9]
proc zero_first input xs mut slice u8 . output usize . effects state .
requires gt len xs. . 0 . .
do
set idx xs. 0 . 0 .
return len xs. . .
end .
실행 결과
$ lowentc --run zero_first mutproc.low [7,8,9]
zero_first([0,8,9]) = 3
arg0 (written) = [0,8,9]
effects state . 는 이 op 이 자기 바깥의 상태를 고친다는 선언이다. VM 이 보여 주는 arg0 (written) = [0,8,9] 가 그 흔적이다 — 호출자가 넘긴 슬라이스의 첫 원소가 바뀌었다.
흔한 오해. proc 은 fn 보다 느리다
fn 에는 차례 바꾸기·공통 부분식 제거·기억해 두기 같은 최적화가 허락되고, proc 에는 허락되지 않는다. 순수한 일을 proc 으로 적으면 그 기회를 스스로 버리는 것이다.5.3 fn 에는 effects 를 적지 않는다#
fn 의 효과는 이미 none 이다. 그래서 effects none . 을 덧붙이면 거절된다.
examples/ch05/redundant.low
module redundant .
rem expect: E-EFFECT-REDUNDANT
fn square input a u64 . output u64 . effects none .
do
return mul a. a. . .
end .
실행 결과
$ lowentc --check redundant.low
redundant.low:4:0 E-EFFECT-REDUNDANT: a `fn` is pure by contract, so `effects none .` says nothing that the `fn` did not already say — DROP the clause. One meaning must have one spelling (SPEC-002 §2.5). (Keep writing `effects` on `proc`: there it NARROWS, and omitting it means unrestricted.)
뜻이 틀려서가 아니라 한 뜻에 두 표기가 되기 때문이다. proc 에서는 사정이 다르다. proc 의 effects 절은 좁히는 일을 한다. 그래서 proc 은 절을 반드시 적는다 — 없으면 E-EFFECT-MISSING 이고, 효과가 없으면 effects none . 이다. lowentc --doc 이 몸이 실제로 내는 효과(inferred:)를 알려 준다. 적은 것보다 많은 일을 하면 거절되며, 적어 놓고 하지 않는 효과도 알려진다.
문. proc 인데 실제로 아무 효과도 내지 않으면 어떻게 되는가?
답. proc 에 effects none . 을 적는 것은 허락된다. 지역 가변 상태나 반복을 강조하고 싶어서 proc 으로 두는 경우가 옛 코드에 있다. 그러나 관측되는 효과가 없다면 fn 으로 적는 것이 이 언어의 권하는 모양이다. 순수함이 머리에 드러나야 호출자와 컴파일러가 그것을 쓸 수 있다. 내보내지 않은 proc 이 effects none 이고 mut 입력도 없고 다른 proc 도 안 부르면 처리기가 W-PROC-PURE 로 알려 준다(RFC-0113 O3).
5.4 효과는 부르는 쪽으로 번진다#
op 을 부르면 그 op 의 효과가 부르는 쪽의 효과에 더해진다. 순수한 fn 이 io 를 내는 proc 을 부르면, 그 fn 은 더 이상 순수하지 않다.
examples/ch05/calls.low
module calls .
rem expect: E-EFFECT-CALC
use stdio .
proc say input out cap io . output usize . effects io .
do
return stdio.write out. 1 "hi\n" . .
end .
fn helper input out cap io . output usize .
do
return say out. . .
end .
실행 결과
$ lowentc --check calls.low
calls.low:12:1 E-EFFECT-CALC: this fn is declared pure but performs `io` — make it a `proc` with `effects …`, or remove the effect
helper 는 스스로 출력하지 않는다. 출력하는 것은 say 다. 그래도 거절된다. 이 규칙 덕분에 op 의 머리만 보고도 그 op 이 부르는 모든 op 의 효과를 알 수 있다. 효과를 숨긴 채 전달하는 길이 없다. 어떤 효과가 어떤 권한과 짝지어지는지는 15·16장에서 다룬다.
5.5 매개변수와 반환#
op 하나를 조각조각 뜯어 보면 이렇다. 처음 보는 사람이 가장 헷갈리는 것은 절마다 마침표로 끝난다는 점이다. 마침표는 문장의 끝처럼 “이 절은 여기까지” 를 알린다.
fn add3 input a i32 . input b i32 . input c i32 . output i32 .
│ │ └─ 매개변수 a 의 타입은 i32 ─┘ └ 돌려주는 값의 타입
│ └─ 이름 (부를 때 쓴다)
└─ 갈래: fn(순수) 또는 proc(효과를 낼 수 있음)
do ← 몸이 여기서 시작한다
return add add a. b. . c. . . ← 계산해서 돌려준다. 값도 문장도 제 점으로 닫힌다
end . ← 몸이 끝나고, 점이 선언 전체를 닫는다부를 때는 이름 뒤에 인자를 공백으로 늘어놓고, 그 호출을 닫는 점을 찍는다. add3 1 2 3 . 이 한 번의 호출이다. 호출을 다른 호출의 인자로 넣으면 안쪽 호출이 제 점으로 먼저 닫힌다 — add3 add3 1 2 3 . 4 5 .. 안쪽 호출을 눈에 띄게 하려면 괄호를 둘러도 된다 (add3 (add3 1 2 3 .) 4 5 .). 괄호는 뜻을 바꾸지 않는다. C 처럼 add3(1, 2, 3) 으로 쓰는 법은 없다. 쉼표는 이 언어에서 뜻이 없는 기호이고, 그래서 거절된다(아래 «흔한 실수»).
매개변수는 input <이름> <타입> . 한 절에 하나다. 여럿이면 절을 되풀이한다. 부르는 쪽은 절의 차례대로 인자를 준다. 돌려주는 값은 output <타입> . 하나이고, 본문에서 return <식> . 으로 돌려준다.
fn compare input a i32 . input b i32 . output i32 .
do
return sub a. b. . .
end .타입 앞에 붙는 낱말이 소유와 가변과 유무를 말한다. 자주 만나는 것은 다음과 같다.
| 적는 것 | 뜻 |
|---|---|
input xs slice u8 . | 남의 바이트를 읽기로 빌린다 |
input xs mut slice u8 . | 남의 바이트에 쓸 수 있게 빌린다(proc 이어야 한다) |
input p ref point . · mut_ref point | 값 하나를 읽기로·쓰기로 빌린다(12장) |
input o option u64 . | 있을 수도 없을 수도 있는 값(11장) |
input out cap io . | 권한(16장) |
input comptime t type . | 번역할 때 정해지는 타입(22장) |
표 5.1 — 매개변수 타입에 붙는 낱말
반환값은 하나다. 여럿을 돌려주려면 struct 로 묶거나(10장), 호출자가 넘긴 mut 슬라이스에 쓴다.
출력에 이름을 붙일 수도 있다 — output <이름> <타입> .. 그 이름은 몸 안의 지역이고 0 에서 시작한다. 수는 0, bool 은 false, option 은 none, 구조체는 칸마다 0 이다. 몸이 그 이름에 쓰고 return <이름> . 으로 돌려준다. 결과를 조금씩 쌓는 op 에서 «먼저 0 으로 채운 지역을 하나 만든다» 는 줄이 사라진다.
fn total input xs slice u64 . output t u64 .
do
for x xs. do
set t. add t. x. . .
end .
return t. .
end .output 뒤의 첫 낱말이 타입 이름이면 출력 전체가 타입이고(output result u64 perr .), 타입 이름이 아니면 결과의 이름이다. 0 이 정해지지 않은 타입(슬라이스 · result · enum 같은 것)에는 이름을 붙일 수 없다(E-RESULT-NOZERO) — 그런 출력은 이름 없이 적고 값을 돌려준다. ensures 는 여전히 ret 으로 결과를 가리킨다.
배열도 돌려줄 수 있다 — output r array 3 u64 .. 배열에는 값으로 돌려주는 길이 없어서 부르는 쪽이 자리를 준다. 부르는 쪽은 결과를 이름에 묶고(let s array 3 u64 squares n. . .), 부름 받는 쪽은 그 이름의 바이트에 바로 짓는다. 그래서 식 한가운데서 부를 수는 없다(idx squares n. . 0 . 은 E-RESULT-PLACE) — 먼저 이름에 묶는다.
fn squares input n usize . output r array 3 u64 .
requires le n. 1000 . .
do
repeat i usize 3 do
set idx r. i. . cast u64 mul add n. i. . add n. i. . . . .
end .
return r. .
end .5.6 neg 만 단항이다#
사칙 연산 add·sub·mul·div·mod 는 인자를 둘 받는다. 산술에서 인자를 하나 받는 것은 부호를 뒤집는 neg 뿐이다.
examples/ch05/neg.low
module negs .
rem run: flip 5
rem run: dist -3 4
fn flip input a i64 . output i64 .
do
return neg a. . .
end .
fn dist input a i64 . input b i64 . output i64 .
do
return abs sub a. b. . . .
end .
실행 결과
$ lowentc --run flip neg.low 5
flip(5) = -5
$ lowentc --run dist neg.low -3 4
dist(-3, 4) = 7
neg 는 부호 있는 타입의 일이다. 부호 없는 값에 쓰면 넘침이다. abs 는 절댓값이고, i64 의 가장 작은 값처럼 양수로 표현할 수 없는 값을 주면 멈춘다. expr 섬 안에도 단항 연산자는 없어서, 부호를 뒤집으려면 expr 0 - a 로 적거나 (neg a) 를 괄호로 부른다.
5.7 수식어#
op 머리 앞에 수식어를 붙일 수 있다.
export— 다른 모듈에서 부를 수 있게 내보낸다. 네이티브로 낼 때는 C 에서 부를 수 있는 심볼이 된다. 붙이지 않은 op 은 그 모듈 안에서만 쓴다. 내보내는 것은 선택이지 부작용이 아니어야 하기 때문이다.extern— 구현이 바깥(C)에 있다. 본문 대신link절로 이름을 댄다(29장).unsafe— 언어가 검사할 수 없는 일을 한다.effects unsafe와 짝이다.
unsafe extern proc c_area do
input k cap c .
input w i64 .
input h i64 .
output i64 .
effects unsafe .
link lw_c_area .
end .5.8 op 안의 op#
한 곳에서만 쓰는 작은 op 은 쓰는 자리 가까이에 둘 수 있다. op 본문 바로 아래에 fn·proc 을 선언하면 그 op 안에서만 보이는 로컬 op 이 된다. 이름이 짧아도 다른 op 과 부딪치지 않는다.
examples/ch05/localop.low
module localop .
rem run: count_selected [3,7,1,15] 3
rem run: parity 7
rem 쓰는 자리 가까이에 둔 보조 op --- 이 op 안에서만 보인다
export fn count_selected input xs slice u8 . input mask u8 . output u64 .
do
fn has_bits input x u8 . input bits u8 . output bool .
do
return eq bit_and x. bits. . bits. . .
end .
return pipe xs. do
filter has_bits with mask. .
count .
end . .
end .
rem 선언 앞에서 불러도 되고, 형제끼리 서로 불러도 된다
export fn parity input n u64 . output u64 .
do
fn is_even input k u64 . output u64 .
do
if eq k. 0 . do
return 1 .
end .
return is_odd sub k. 1 . . .
end .
fn is_odd input k u64 . output u64 .
do
if eq k. 0 . do
return 0 .
end .
return is_even sub k. 1 . . .
end .
return is_even n. . .
end .
실행 결과
$ lowentc --run count_selected localop.low [3,7,1,15] 3
count_selected([3,7,1,15], 3) = 3
arg0 (written) = [3,7,1,15]
$ lowentc --run parity localop.low 7
parity(7) = 0
- 로컬 op 은 보통 op 과 같다 — 머리 절, 몸, 효과, 계약 규칙이 모두 같다. 선언 앞에서 불러도 되고, 같은 op 의 로컬끼리 서로 불러도 된다(
is_even과is_odd). - 다른 op 은 그 이름을 보지 못한다. 서로 다른 op 이 같은 로컬 이름을 따로 쓸 수 있다.
- 로컬 op 은 바깥 op 의 매개변수나 지역을 보지 않는다. 필요한 값은 매개변수로 건넨다. 그래서 클로저가 아니고, 실행 중에 만들어지는 것도 숨은 인자도 없다.
if·반복 안이나 다른 로컬 op 안에는 둘 수 없고(E-LOCAL-PLACE),export를 달 수 없다(E-LOCAL-EXPORT).
examples/ch05/mistake_capture.low
module mistake_capture .
rem expect: E-LOCAL-CAPTURE
rem ✘ 로컬 op 은 바깥의 `limit` 을 보지 않는다 --- 매개변수로 건넨다
export fn count_over input xs slice u8 . input limit u8 . output u64 .
do
fn over input x u8 . output bool .
do
return gt x. limit. . .
end .
return pipe xs. do
filter over .
count .
end . .
end .
실행 결과
$ lowentc --check mistake_capture.low
9:18 E-LOCAL-CAPTURE: a local op does not see the enclosing op's parameters, locals, regions or borrows — it is an ordinary op declared close to its use, with no hidden capture. Pass the value in as an input (RFC-0121 §6.6)
바깥의 limit 을 쓰고 싶으면 over 에 input limit u8 . 을 더하고 pipe 에서 filter over with limit . 로 건넨다 (24장).
5.9 흔한 실수#
처음 op 을 쓸 때 누구나 한 번쯤 밟는 자리들이다. 모두 번역할 때 걸리므로, 진단 코드를 보고 이 절로 돌아오면 된다.
반례. C 처럼 괄호와 쉼표로 부른다
다른 언어에 익숙하면 손이 먼저 add3(1, 2, 3) 을 친다.
examples/ch05/mistake_ccall.low
module mistake_ccall .
rem expect: E-VOCAB-REMOVED
fn add3 input a i32 . input b i32 . input c i32 . output i32 .
do
return add add a. b. . c. . .
end .
fn use3 output i32 .
do
rem ✘ C 처럼 괄호와 쉼표로 불렀다 --- Lowent 의 호출은 `이름 인자 인자 …` 다
return add3(1, 2, 3) .
end .
실행 결과
$ lowentc --check mistake_ccall.low
12:16 E-VOCAB-REMOVED: `,` (R3) was removed — RFC-0103, 2026-08-27. It opened the NEXT operand of the same form, but nothing used it that way: every `,` in the corpus was a LINE CONTINUATION, and newlines no longer close a form, so continuing a line needs nothing at all. A form is `head operand*` and ends at its closer `.` — just write the operands, on as many lines as you like.
12:19 E-VOCAB-REMOVED: `,` (R3) was removed — RFC-0103, 2026-08-27. It opened the NEXT operand of the same form, but nothing used it that way: every `,` in the corpus was a LINE CONTINUATION, and newlines no longer close a form, so continuing a line needs nothing at all. A form is `head operand*` and ends at its closer `.` — just write the operands, on as many lines as you like.
Lowent 의 호출은 이름 다음에 인자를 공백으로 늘어놓는 전위 표기다. 쉼표는 한때 쓰였다가 없앤 기호라서 E-VOCAB-REMOVED 가 나온다. 괄호·쉼표 없이 부르는 까닭은 기호를 줄여 누구나(휴대전화 자판으로도) 같은 모양으로 쓰게 하려는 것이다. 고치는 법: return add3 1 2 3 . .
반례. 인자 수가 input 절 수와 다르다
examples/ch05/mistake_arity.low
module mistake_arity .
rem expect: E-IR-ARITY
fn add3 input a i32 . input b i32 . input c i32 . output i32 .
do
return add add a. b. . c. . .
end .
fn use3 output i32 .
do
rem ✘ input 절이 셋인데 인자를 둘만 줬다
return add3 1 2 . .
end .
실행 결과
$ lowentc --check mistake_arity.low
mistake_arity.low:12:0 E-IR-ARITY: `add3` takes 3 arguments but 2 are given here — an op call is `<name> <arg>…` with exactly as many arguments as its `input` clauses
input 절 하나가 인자 하나다. 기본값 인자나 인자 수가 달라지는 op 은 없다 — 머리를 보면 부르는 모양이 하나로 정해져야 읽는 사람이 헷갈리지 않기 때문이다. 진단이 몇 개를 받고 몇 개를 주었는지 말해 준다.
반례. 어느 갈래에서 return 을 빠뜨린다
examples/ch05/mistake_partial.low
module mistake_partial .
rem expect: E-RETURN-PARTIAL
fn sign input a i32 . output i32 .
do
if gt a. 0 . do
return 1 .
end .
rem ✘ a 가 0 이하일 때 돌려줄 값이 없다 --- 끝까지 내려오는 길이 있다
end .
실행 결과
$ lowentc --check mistake_partial.low
mistake_partial.low:4:0 E-RETURN-PARTIAL: this op says it OUTPUTS a value, but some path through its body reaches the end without a `return`. Until now the tool quietly returned 0 there — a value that appears NOWHERE in your source (RFC-0019 G-TOTAL). Give every path a `return`, or say `output void` if it really produces nothing. An exhaustive `match` whose every arm returns counts as returning
output 을 적은 op 은 모든 길에서 값을 돌려주어야 한다. 옛 도구는 빠진 자리에서 조용히 0 을 돌려주었는데, 그 0 은 소스 어디에도 적혀 있지 않은 값이라 버그를 숨겼다. 그래서 지금은 E-RETURN-PARTIAL 로 거절한다. 고치는 법은 if 뒤에 나머지 경우의 return 을 적는 것이다 — 예를 들면 return 0 . 을 end 앞에 둔다.
반례. 기본 연산 의 이름으로 op 을 짓는다
examples/ch05/mistake_builtin.low
module mistake_builtin .
rem expect: E-NAME-BUILTIN
rem ✘ `min` 은 이미 내장 op 의 이름이다 --- 이 선언은 영영 불리지 않는다
fn min input a u64 . input b u64 . output u64 .
do
return add a. b. . .
end .
실행 결과
$ lowentc --check mistake_builtin.low
mistake_builtin.low:5:0 E-NAME-BUILTIN: this name is a CORE OP — the resolver always picks the core op, so your declaration can never be called: it exists and does not exist. The namespace is FLAT (no shadowing). Rename it
이름 공간은 평평해서 같은 이름이 둘일 수 없다. 기본 연산(add·min·len·ok …)의 이름을 쓰면 그 선언은 영영 불리지 않으므로 E-NAME-BUILTIN 으로 알려 준다. 지역 이름에도 같은 규칙이 붙는다. 기본 연산 이름의 목록은 부록 A 에 있다.
흔한 오해. 결과를 받지 않아도 fn 을 부르면 무언가 일어난다
fn 은 순수하므로 결과를 버리면 아무 일도 일어나지 않은 것과 같다. 컴파일러는 그런 호출을 지워도 된다.
examples/ch05/discard.low
module discard .
rem expect: E-VALUE-DISCARDED
fn twice input a u64 . output u64 .
do
return mul a. 2 . .
end .
fn caller output u64 .
do
rem ✘ 결과를 받지 않은 fn 호출 --- 순수하므로 아무 일도 안 한다. 그래서 거절된다
twice 5 .
rem 결과를 쓰려면 이름에 묶는다
let t u64 twice 5 . .
return t. .
end .
실행 결과
$ lowentc --check discard.low
discard.low:12:0 E-VALUE-DISCARDED: this statement computes a value and drops it — nothing changes. Did you mean `set <name> (…) .`? (A pure op or a `fn` call has no effect to be a statement for — RFC-0113 R5; to put a number into a bitset write `bitset_insert`)
첫 twice 5 . 는 계산만 하고 결과를 버린다 — 아무 일도 안 하는 문장이라 처리기가 E-VALUE-DISCARDED 로 거절한다(RFC-0113 R5). 결과를 쓰려면 둘째 줄처럼 이름에 묶는다. 무언가를 하려는 호출(출력·쓰기)이라면 그것은 proc 이어야 하고, 그러면 머리에 효과가 적힌다.
5.10 이 장의 문법을 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
fn f input a T . output R . do … end . | 순수한 op | 결과를 기억·재배치·생략해도 되는지 머리만 보고 알게 |
proc f … effects E . do … end . | 효과를 낼 수 있는 op | 무엇을 하는지(E)가 머리에 드러나게 |
input x T . | 매개변수 하나 | 절마다 하나라 이름과 타입이 줄마다 보인다 |
output T . · output void . | 돌려주는 값의 타입 · 돌려주는 값 없음 | 반환은 하나 — 여럿이면 struct 로 묶는다 |
output r T . | 이름 붙은 결과 — 0 에서 시작하는 지역 | 결과를 쌓는 op 에서 «0 으로 채운 지역» 줄이 사라진다 |
return e . | 값을 돌려주고 끝낸다 | 모든 길에서 적어야 한다(E-RETURN-PARTIAL) |
f a b c | 부르기 — 이름 뒤에 인자를 공백으로 | 괄호·쉼표 없는 전위 표기 |
f g a. . b. . | 호출을 인자로 넣기 | 괄호는 “이 안이 값 하나” 라는 표시 |
requires c . | 부르는 쪽이 지켜야 할 조건 | 어긴 쪽의 잘못임을 진입에서 알린다(14장) |
effects state . | 호출자의 저장소를 고친다 | mut 매개변수로 쓰는 op 의 머리 |
neg a. . | 부호 뒤집기(유일한 단항 산술) | 빼기와 뒤섞이지 않게 이름으로 |
export · extern · unsafe | 내보내기 · 몸이 C 에 · 검사 못 하는 일 | 할 수 있는 일이 머리 앞에 보이게 |
do 바로 아래의 fn g … end . | 로컬 op — 그 op 안에서만 보인다 | 바깥 이름을 보지 않는다 — 클로저가 아니다 |
표 5.2 — op 의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
fn 이거나 proc 이고 갈래는 언제나 적는다. 순수함은 관측적이어서 op 안에 갇힌 변이는 순수로 치지만, mut 매개변수로 호출자에게 보이는 쓰기는 proc 이어야 한다. fn 에는 effects 를 적지 않고, proc 의 effects 는 좁히는 절이다. 효과는 부르는 쪽으로 번진다. 매개변수는 절마다 하나, 반환은 하나이며, 산술의 단항은 neg 뿐이다.