7 흐름 — 갈래, 되풀이, 빠져나가기
먼저 알아야 할 것
if 는 값을 내지 않는 문장이다돌아보기
6장에서 let x u64 if gt a 1 . 5 else 6 . 은 왜 거절되었고, 갈래마다 값을 정하려면 어떻게 적는가?
답. if 는 값을 내지 않는 문장이라서 식의 자리에 둘 수 없다(E-IF-VALUE). 갈래마다 값을 정하려면 var 를 기본값으로 짓고 갈래 안에서 set 하거나, 갈래마다 return 한다. 이 장은 그 갈래 자체와, 되풀이와 빠져나가기의 규칙을 다룬다.
이 장의 필요성과 맥락
guard 의 else 는 반드시 떠나야 하며, 값을 돌려주는 op 은 모든 길에서 돌려주어야 하고, match 는 모든 경우를 덮어야 한다. 이 검사들은 “어떤 길로 가면 값이 없다” 는 결함을 번역할 때 없앤다. 데이터(제3부)로 넘어가기 전에 흐름의 약속을 먼저 세운다.이 장이 끝나면
if … else, while, for 와 수를 세는 repeat·range·cycle, break·continue 를 쓰는 법을 익힌다. guard 가 조건을 그 아래 코드에 대한 사실로 바꾸는 방식과, else 가 떠나지 않으면 왜 거절되는지 알게 된다. 모든 길이 값을 돌려주어야 한다는 규칙, 수와 범위를 가르는 match 와 그 망라 검사, 그리고 panic 이 효과라는 것도 보게 된다.이 장에서 답할 질문
match가if사슬보다 나은 점은 망라 검사 말고 무엇인가?
7.1 조건과 되풀이#
if 는 조건이 참일 때 블록을 실행하고, else 로 거짓일 때의 블록을 적는다. while 은 조건이 참인 동안 되풀이하고, for 는 슬라이스의 원소를 차례로 훑는다. 조건은 모두 bool 이어야 한다.
examples/ch07/loops.low
module loops .
rem run: count_big 10
rem run: first_zero [5,3,0,9]
rem run: first_zero [5,3]
rem run: odd_sum [1,2,3,4,5]
fn count_big input n u32 . output u32 .
requires le n. 100 . .
do
var total u32 0 .
var i u32 0 .
while lt i. n. . do
if gt i. 5 . do
set total. add total. 1 . .
end .
set i. add i. 1 . .
end .
return total. .
end .
fn first_zero input xs slice u8 . output usize .
do
var i usize 0 .
while lt i. len xs. . . do
if eq idx xs. i. . 0 . do
break .
end .
set i. add i. 1 . .
end .
return i. .
end .
fn odd_sum input xs slice u8 . output u64 .
do
var acc u64 0 .
for x xs. do
if eq mod x. 2 . 0 . do
continue .
end .
set acc. add acc. widen u64 x. . . .
end .
return acc. .
end .
실행 결과
$ lowentc --run count_big loops.low 10
count_big(10) = 4
$ lowentc --run first_zero loops.low [5,3,0,9]
first_zero([5,3,0,9]) = 2
arg0 (written) = [5,3,0,9]
$ lowentc --run first_zero loops.low [5,3]
first_zero([5,3]) = 2
arg0 (written) = [5,3]
$ lowentc --run odd_sum loops.low [1,2,3,4,5]
odd_sum([1,2,3,4,5]) = 9
arg0 (written) = [1,2,3,4,5]
count_big은 0 부터n미만까지 세면서 5 보다 큰 수만 센다.while lt i. n. . do … end .의 모양을 보면, 조건은 몸을 여는do에서 끝난다 — 조건과do사이에 마침표가 없다. 마침표는 문장을 닫는 일만 한다.first_zero는 0 을 만나면break로 되풀이를 벗어난다. 끝까지 없으면 길이를 돌려준다.break와continue는 가장 안쪽 되풀이에 듣는다. 바깥 되풀이까지 닿으려면 뒤에 수를 적는다(아래 «겹친 되풀이를 한 번에 벗어난다»).while의 조건은 바퀴마다 몸에 들어가기 전에 본다. 처음부터 거짓이면 몸은 한 번도 돌지 않는다.odd_sum은for x xs. do … end .로 원소를 훑고, 짝수면continue로 다음 원소로 넘어간다.for의 이름x는 슬라이스의 원소 타입(u8)이고 블록 안에서만 산다.
for x in xs 처럼 in 을 적으면 E-VOCAB-REMOVED 로 거절된다. 훑을 대상은 이름 바로 뒤에 온다.
수를 세는 반복은 제 머리말이 있다. repeat i u64 n. do 는 0 부터 n 미만까지, range i u64 a. b. do 는 a 부터 b 까지 두 끝을 넣어 돈다. 다음 값을 직접 적으려면 cycle i u64 1 while lt i. 100 . next mul i. 2 . do 로 쓴다. 셈의 타입(u64)은 늘 적는다. 머리 안에는 마침표가 없다 — 식은 do·step·while·next 같은 낱말에서 끝난다. 원소를 거르려면 do 앞에 if <조건> 을 붙인다(for x xs. if gt x. 2 . do). for 는 원소를 도는 한 가지 일만 한다 — 옛 꼴 for i count u64 n . do 는 E-FOR-OLD 로 거절된다.
examples/ch07/counting.low
module counting .
rem run: tally 4
rem 수를 세는 되풀이 --- `repeat` 는 끝을 빼고, `range` 는 두 끝을 넣는다
export fn tally input n u64 . output u64 .
do
var s u64 0 .
repeat i u64 n. do
set s. add s. i. . .
end .
range j u64 10 1 step -3 do
set s. add mul s. 100 . j. . .
end .
return s. .
end .
실행 결과
$ lowentc --run tally counting.low 4
tally(4) = 610070401
repeat i u64 n은n = 4이면 0 · 1 · 2 · 3 을 돈다.n이 0 이면 한 번도 돌지 않는다.range j u64 10 1 step -3은 10 · 7 · 4 · 1 이다.step을 적지 않으면 두 끝이 방향을 정한다(range j u64 10 3은 10 부터 3 까지 내려간다). 셈은 넘치지 않는다 —range j u8 0 255는 255 에서 끝난다. 다음 값이 끝을 지나치면 거기서 끝난다(range j u8 250 255 step 3은 250 · 253).- 머리가 읽은 것(끝 ·
step)은 들어갈 때 한 번 계산한다. 몸 안에서 셈 이름i나 머리가 읽은 이름n을set하면E-FOR-HEAD다.step 0이나 부동소수점 셈 타입은E-FOR-STEP이다. for x mut buf. do set x. 0 . end .는 원소 자리를 돈다 —set x가buf의 그 칸에 쓴다. 몸 안에서는buf를 직접 만지지 않고x로만 만진다(E-FOR-HEAD).- 머리 끝,
do앞에if <조건>을 붙이면 조건이 참인 값만 몸을 돈다:range i u64 1 20 if eq mod i. 3 . 0 . do. - 다음 값을 직접 적는 되풀이는
cycle j u64 1 while le j. 100 . next mul j. 2 . do처럼 적는다 — 1 · 2 · 4 … 64 를 돈다. 몸 안의continue도next를 거친다.
examples/ch07/doubling.low
module doubling .
rem run: steps 100
rem 다음 값을 직접 적는 되풀이 --- 1 에서 시작해 `limit` 에 닿기 전까지 두 배씩
export fn steps input limit u64 . output u64 .
requires le limit. 1000000 . .
do
var n u64 0 .
cycle i u64 1 while lt i. limit. . next mul i. 2 . do
set n. add n. 1 . .
end .
return n. .
end .
실행 결과
$ lowentc --run steps doubling.low 100
steps(100) = 7
steps 100은 1 · 2 · 4 · 8 · 16 · 32 · 64 일곱 바퀴를 돈다. 128 은lt i. limit. .이 거짓이라 몸에 들어가지 않는다.
7.1.1 겹친 되풀이를 한 번에 벗어난다#
break 와 continue 뒤에 수를 적으면 그 수만큼 바깥으로 센 되풀이에 듣는다. 1 은 가장 안쪽이고(수를 적지 않은 것과 같다), 2 는 그것을 감싼 되풀이다. 되풀이에 이름을 붙이는 라벨은 없다.
examples/ch07/breakout.low
module breakout .
rem run: first_cell 8
rem 겹친 되풀이에서 찾던 칸을 만나면 `break 2 .` 로 둘을 한 번에 벗어난다
export fn first_cell input want u32 . output usize .
do
let g lit slice array 3 u32 lit array 3 u32 1 2 3 . lit array 3 u32 4 0 6 . lit array 3 u32 7 8 9 . . .
var hit usize 99 .
repeat r usize len g. . do
repeat c usize 3 do
if eq idx idx g. r. . c. . want. . do
set hit. add mul r. 3 . c. . .
break 2 .
end .
end .
end .
return hit. .
end .
rem `continue 2 .` 는 안쪽 되풀이를 벗어나 바깥 되풀이의 다음 바퀴로 간다
export fn rows_without input bad u32 . output u64 .
do
let g lit slice array 3 u32 lit array 3 u32 1 2 3 . lit array 3 u32 4 0 6 . lit array 3 u32 7 8 9 . . .
var n u64 0 .
for row g. do
for x row. do
guard ne x. bad. . else continue 2 . .
end .
set n. add n. 1 . .
end .
return n. .
end .
실행 결과
$ lowentc --run first_cell breakout.low 8
first_cell(8) = 7
first_cell 8은 셋째 줄 둘째 칸에서 8 을 만나break 2 .로 두 되풀이를 함께 벗어난다. 답은2 × 3 + 1 = 7이다. 수를 적지 않은break .였다면 안쪽 되풀이만 벗어나고 바깥은 남은 줄을 계속 돈다.rows_without 0은 0 이 든 줄을 만나면continue 2 .로 그 줄의 나머지를 건너뛰고 다음 줄로 간다. 그래서 그 줄은 세지 않는다. 답은 2 다.- 세는 것은 되풀이의 머리(
while·for·repeat·range·cycle)뿐이다.if와match의 블록은 세지 않는다. - 되풀이를 한 겹 감싸거나 풀면 같은 수가 다른 되풀이를 가리킨다. 그때는 수를 다시 센다.
수가 감싼 되풀이보다 많으면 거절된다.
examples/ch07/mistake_breakfar.low
module mistake_breakfar .
rem expect: E-LOOP-COUNT
fn f output u64 .
do
var n u64 0 .
repeat a u64 3 do
repeat b u64 3 do
set n. add n. 1 . .
rem ✘ 감싼 되풀이는 둘뿐이다 --- 셋을 벗어날 수 없다
break 3 .
end .
end .
return n. .
end .
실행 결과
$ lowentc --check mistake_breakfar.low
mistake_breakfar.low:11:0 E-LOOP-COUNT: the number after `break` / `continue` counts the loops around it: 1 is the innermost (the same as writing no number), 2 is the loop around that one, and so on. It is a plain decimal literal from 1 up to the number of loops this statement sits in — there is no loop that far out here
7.2 guard — 조건을 사실로 바꾼다#
guard <조건> else <빠져나감> . 은 조건이 참이 아니면 그 자리에서 떠난다. 떠나는 문장은 return·break·continue·panic 이다.
examples/ch07/guards.low
module guards .
rem run: head_or_zero [7,8]
rem run: head_or_zero []
rem run: grade 95
rem run: grade 42
fn head_or_zero input data slice u8 . output u8 .
do
guard ge len data. . 1 . else return 0 . .
return idx data. 0 . .
end .
fn grade input score u8 . output u8 .
do
guard le score. 100 . else return 0 . .
if ge score. 90 . do
return 65 .
end else do
if ge score. 60 . do
return 66 .
end .
end .
return 70 .
end .
실행 결과
$ lowentc --run head_or_zero guards.low [7,8]
head_or_zero([7,8]) = 7
arg0 (written) = [7,8]
$ lowentc --run head_or_zero guards.low []
head_or_zero([]) = 0
arg0 (written) = []
$ lowentc --run grade guards.low 95
grade(95) = 65
$ lowentc --run grade guards.low 42
grade(42) = 70
head_or_zero 의 guard 를 지난 코드는 슬라이스가 비지 않은 세계에서만 산다. 그래서 idx data. 0 . 이 안전하다. grade 는 guard 로 범위 밖을 먼저 걸러 내고, 그 아래에서 if … end else do … end 로 갈래를 나눈다.
guard 가 if not 의 다른 이름이 아닌 까닭은 else 가 반드시 떠나야 한다는 데 있다. 떠나지 않으면 거절된다.
examples/ch07/fallthrough.low
module fallthrough .
rem expect: E-GUARD-FALLTHROUGH
fn clamp5 input n u32 . output u32 .
do
guard le n. 5 . else do
let capped u32 5 .
end .
return n. .
end .
실행 결과
$ lowentc --check fallthrough.low
fallthrough.low:6:0 E-GUARD-FALLTHROUGH: this `guard`'s `else` FALLS THROUGH — it must LEAVE (`return` / `break` / `continue` / `panic`). If it falls through, the code AFTER the guard wrongly believes the condition holds. Leaving is the WHOLE point of `guard`: it is what lets the rest of the op assume the condition. (SPEC-002 D6 always said so and nothing enforced it — which made `guard` an exact synonym of `if not`.) If you do not mean to leave, write `if`
else 가 떠나지 않고 아래로 흘러내리면, guard 아래 코드는 n 이 5 이하라고 믿지만 실제로는 그렇지 않다. 믿을 수 없는 사실을 믿게 두느니 번역을 거절한다. 떠나지 않을 일이면 if 를 쓴다.
흔한 오해. guard 는 코드를 짧게 하는 문법 설탕이다
guard 의 본뜻은 컴파일러와 읽는 사람에게 사실 하나를 건네는 것이다. 컴파일러는 guard 를 지난 뒤 그 조건을 참으로 알고 경계 검사를 지우는 데 쓴다. 같은 뜻을 if … do return … end 로 적어도 동작은 같지만, 떠남이 문법으로 보장되지는 않는다.7.3 모든 길이 값을 돌려준다#
값을 돌려주는 op 은 모든 길에서 값을 돌려주고 끝나야 한다.
examples/ch07/partial.low
module partial .
rem expect: E-RETURN-PARTIAL
fn sign_flag input a i32 . output u8 .
do
if gt a. 0 . do
return 1 .
end .
end .
실행 결과
$ lowentc --check partial.low
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
a 가 0 이하인 길에는 return 이 없다. 옛 도구는 그 길에서 조용히 0 을 돌려주었다. 소스 어디에도 없는 값이다. 지금은 거절된다. 값을 돌려주고 끝나는 것으로 인정되는 문장은 return, 두 갈래가 모두 돌려주는 if … else, 모든 갈래가 돌려주는 match 다. while·for·guard 는 빠져나가는 길이 있으므로 그 자체로 돌려주는 것으로 보지 않는다. 그래서 반복 뒤에는 늘 return 이 온다.
돌려줄 값이 없는 op(output void)은 return 없이 몸의 끝까지 가도 된다. 끝나는 자리가 곧 돌아가는 자리다.
7.4 match — 경우를 빠짐없이#
match 는 값을 경우별로 가른다. if 사슬과 다른 점은 모든 경우를 다뤄야 한다는 것이다.
examples/ch07/bands.low
module bands .
rem run: band 9
rem run: band 10
rem run: band 200
rem run: half 128
rem run: big 101
fn band input x u8 . output u8 .
do
match x. do
case 0 to 9 do return 1 . end .
case 10 to 19 do return 2 . end .
case _ do return 0 . end .
end .
end .
fn half input b u8 . output u8 .
do
match b. do
case 0 to 127 do return 0 . end .
case 128 to 255 do return 1 . end .
end .
end .
fn big input x u8 . output u8 .
do
match x. do
case y when gt y 100 do return 1 . end .
case _ do return 0 . end .
end .
end .
실행 결과
$ lowentc --run band bands.low 9
band(9) = 1
$ lowentc --run band bands.low 10
band(10) = 2
$ lowentc --run band bands.low 200
band(200) = 0
$ lowentc --run half bands.low 128
half(128) = 1
$ lowentc --run big bands.low 101
big(101) = 1
band는lo to hi범위(양 끝 포함)로 가른다. 정수는 경우가 많으므로 나머지를 덮는case _가 필요하다.half는 두 범위가u8의 0 … 255 를 빈틈 없이 덮으므로_가 없어도 된다.big의case y when gt y 100은 값 전체를y에 묶고 가드로 더 좁힌다. 가드가 붙은 갈래는 거짓일 수 있으므로 망라에 세지 않는다. 그래서_가 여전히 필요하다.
빈틈이 있으면 거절된다.
examples/ch07/inexhaustive.low
module inexhaustive .
rem expect: E-MATCH-INEXHAUSTIVE
fn band input x u8 . output u8 .
do
match x. do
case 0 to 9 do return 1 . end .
case 11 to 255 do return 2 . end .
end .
end .
실행 결과
$ lowentc --check inexhaustive.low
inexhaustive.low:6:0 E-MATCH-INEXHAUSTIVE: this `match` on an integer is not exhaustive — the literals/ranges leave a gap, so a `case _ .` wildcard (or `else`) is required, OR the ranges must TILE the whole domain of the scrutinee's type (e.g. `0 to 127` + `128 to 255` on a u8) (RFC-0020 §6.4; MM4 tiling)
10 이 어느 갈래에도 들지 않는다. 같은 경우를 두 번 적거나 _ 뒤에 갈래를 두면 E-MATCH-REDUNDANT 로 거절된다. 경고가 아니라 오류다 — 한 번도 실행될 수 없는 갈래는 대개 결함을 숨긴다.
문. match 가 if 사슬보다 나은 점은 망라 검사 말고 무엇인가?
답. 나중에 경우가 늘어날 때 드러난다. 열거형에 변형을 하나 더하면, 그 열거형을 가르는 모든 match 가 번역에서 멈춰 고쳐야 할 자리를 알려 준다(10장). if 사슬은 새 경우를 조용히 마지막 else 로 흘려보낸다. 또 --ir 로 보면 도구가 갈래 수와 최악 비교 횟수 같은 디스패치 비용을 말해 준다.
7.5 panic 은 효과다#
panic 은 프로그램을 즉시 멈춘다. 되돌릴 수 없는 멈춤이므로 효과이고, 쓰는 op 은 effects panic 을 적는 proc 이어야 한다.
examples/ch07/panics.low
module panics .
rem run: checked 3
rem trap: checked 0
proc checked input n u32 . output u32 . effects panic .
do
if eq n. 0 . do
panic "n must not be zero" .
end .
return div 100 n. . .
end .
실행 결과
$ lowentc --run checked panics.low 3
checked(3) = 33
$ lowentc --run checked panics.low 0
== ir diagnostics (1) ==
0:0 E-VM-PANIC: the program called `panic` — this is an unrecoverable stop, and it is NOT a contract violation (the code chose to stop, it did not break a promise)
VM 은 E-VM-PANIC 을 내며, 이것이 계약 위반이 아니라는 것을 분명히 한다 — 코드가 멈추기로 고른 것이지 약속을 어긴 것이 아니다. 순수한 fn 은 panic 을 쓸 수 없다. 다만 넘침이나 계약 위반으로 처리기가 일으키는 멈춤은 fn 에서도 일어난다. 그것은 op 이 한 일이 아니라 처리기가 약속을 지키게 한 일이다.
panic 은 복구할 수 없는 상황에만 쓴다. 호출자가 다룰 수 있는 실패는 값으로 돌려준다 (11장). 둘을 가르는 기준은 17장이 다룬다.
7.6 흔한 실수#
반례. for x in xs 로 적는다
examples/ch07/mistake_forin.low
module mistake_forin .
rem expect: E-VOCAB-REMOVED
fn total input xs slice u8 . output u64 .
do
var t u64 0 .
rem ✘ `in` 을 적었다 --- `for` 는 `for <이름> <슬라이스> do` 다
for x in xs. do
set t. add t. widen u64 x. . . .
end .
return t. .
end .
실행 결과
$ lowentc --check mistake_forin.low
8:9 E-VOCAB-REMOVED: `in` is gone — a loop over elements is `for <name> <slice>. do … end .` — found `in` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
for 는 for <이름> <슬라이스> do 다. 훑을 대상이 이름 바로 뒤에 오고, 몸의 시작은 do 가 알리므로 in 이 전할 것이 없다. 같은 뜻에 표시를 하나 더 두지 않으려고 없앴다. 고치는 법: for x xs. do.
반례. 조건과 do 사이에 마침표를 하나 더 찍는다
examples/ch07/mistake_dotdo.low
module mistake_dotdo .
rem expect: E-CLOSER-EXTRA
fn big input a u64 . output u64 .
do
rem ✘ 조건 뒤에 마침표를 하나 더 찍었다 --- 조건은 제 점으로 이미 닫혔고, 그 다음은 `do` 다
if gt a. 3 . . do
return 1 .
end .
return 0 .
end .
실행 결과
$ lowentc --check mistake_dotdo.low
7:16 E-CLOSER-EXTRA: this stop closes nothing — the value before it is already closed, and the block `do … end` comes next — found `.` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
조건은 값이고, 값은 제 점으로 닫힌다 — gt a. 3 . 의 마지막 점이 그것이다. 그 다음에는 do 가 온다. 점을 하나 더 찍으면 닫을 것이 없는 점이라 E-CLOSER-EXTRA 로 거절된다. while·match 의 머리와 guard 의 조건도 같다. guard gt a. 0 . else return 0 . . 에서 점 셋은 차례로 조건, return, guard 를 닫는다. 고치는 법: if gt a. 3 . do.
반례. 수를 for … count 로 센다
examples/ch07/mistake_forcount.low
module mistake_forcount .
rem expect: E-FOR-OLD
fn tally input n u64 . output u64 .
do
var s u64 0 .
rem ✘ 옛 꼴 --- 수를 세는 일은 `repeat` 가 한다. `for` 는 원소만 돈다
for i count u64 n. do
set s. add s. i. . .
end .
return s. .
end .
실행 결과
$ lowentc --check mistake_forcount.low
8:3 E-FOR-OLD: `for` walks elements only — counting is `repeat <name> <type> <n>. do … end .`, a range is `range <name> <type> <a>. <b>. do … end .` — found `for` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
for 는 원소를 도는 한 가지 일만 한다. 수를 세는 일은 repeat, 구간은 range, 다음 값을 직접 적는 되풀이는 cycle 이 한다 — 머리말을 보면 무슨 되풀이인지 안다. 고치는 법: repeat i u64 n. do.
반례. elif 로 갈래를 잇는다
examples/ch07/mistake_elif.low
module mistake_elif .
rem expect: E-DOT-MISSING
fn grade input n u64 . output u64 .
do
if lt n. 50 . do
return 0 .
rem ✘ `elif` 는 낱말이 아니다 --- 이어지는 갈래는 `end else if … do` 다
end elif lt n. 80 . do
return 1 .
end .
return 2 .
end .
실행 결과
$ lowentc --check mistake_elif.low
9:7 E-DOT-MISSING: a stop `.` must close `if … end` — found `elif` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
elif·elsif·else if: 는 언어마다 다르다. Lowent 는 이미 있는 낱말을 이어 붙인다 — 앞 블록을 end 로 닫고 else if 를 붙인다. elif 는 낱말이 아니다. end 뒤에는 else 가 오거나 폼을 닫는 점이 와야 하므로, elif 를 만난 자리에서 E-DOT-MISSING 으로 거절된다.
examples/ch07/elseif.low
module elseif .
rem run: grade 30
rem run: grade 70
rem run: grade 95
fn grade input n u64 . output u64 .
do
if lt n. 50 . do
return 0 .
rem 갈래를 이을 때는 앞 블록을 `end` 로 닫고 `else if` 를 붙인다
end else if lt n. 80 . do
return 1 .
end else do
return 2 .
end .
end .
실행 결과
$ lowentc --run grade elseif.low 30
grade(30) = 0
$ lowentc --run grade elseif.low 70
grade(70) = 1
$ lowentc --run grade elseif.low 95
grade(95) = 2
마지막 갈래는 end else do … end . 이다. 갈래가 셋 이상이고 모두 한 값을 가르는 것이면 match 가 더 알맞다.
반례. C 처럼 블록 안에 else 를 둔다
examples/ch07/mistake_innerelse.low
module mistake_innerelse .
rem expect: E-STMT-ELSE
fn pick input a u8 . output u8 .
do
var r u8 0 .
if gt a. 5 . do
set r. 1 .
rem ✘ C 처럼 블록 안에 `else` 를 한 줄로 두었다 --- `end else do` 로 앞 블록을 닫아야 한다
else
set r. 2 .
end .
return r. .
end .
실행 결과
$ lowentc --check mistake_innerelse.low
10:3 E-STMT-ELSE: `else` belongs after the `end` of the block before it — `if c. do … end else do … end .` — found `else` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
C 나 몇몇 언어는 if … { … } else { … } 처럼 else 가 앞 블록 뒤에 붙는다. Lowent 에서 else 는 앞 블록을 end 로 닫은 뒤에 온다(end else do). 블록 안에 else 를 한 줄로 두면 E-STMT-ELSE 로 거절한다 — 전에는 번역이 통과하고 실행에서야 E-VM-UNSUP 으로 멈췄으며, 네이티브 빌드는 그 op 을 아예 뺐다.
반례. 조건에 < 같은 기호를 쓴다
examples/ch07/mistake_less.low
module mistake_less .
rem expect: E-CHAR
fn upto input n u64 . output u64 .
do
var i u64 0 .
rem ✘ `<` 는 이 언어의 기호가 아니다 --- 비교는 `lt i n` 이다
while i. < n. do
set i. add i. 1 . .
end .
return i. .
end .
실행 결과
$ lowentc --check mistake_less.low
8:12 E-CHAR: unexpected character
비교는 낱말이다 — lt(작다)·le(작거나 같다)·gt·ge·eq·ne. < 는 이 언어가 모르는 글자라 E-CHAR 가 난다. 기호의 우선순위를 외우지 않아도 되게 하려는 선택이고, 긴 산술에는 expr 섬이 있다(8장). 고치는 법: while lt i. n. . do.
반례. continue 가 증가를 건너뛴다
while 로 세면서 몸 가운데서 continue 하면, 그 아래에 둔 set i. add i. 1 . . 도 함께 건너뛴다.
fn odd_count input xs slice u8 . output u64 .
do
var n u64 0 .
var i usize 0 .
while lt i. len xs. . . do
if eq mod idx xs. i. . 2 . 0 . do
continue .
end .
set n. add n. 1 . .
set i. add i. 1 . .
end .
return n. .
end .첫 짝수에서 i 가 더 이상 오르지 않아 반복이 끝나지 않는다. 번역도 실행 검사도 이것을 잡지 못한다 — 멈추지 않는 것은 넘침이 아니기 때문이다. 증가를 몸의 맨 앞으로 옮기거나(그러면 색인에는 증가 전의 값을 따로 담는다), 원소를 훑는 일이면 처음부터 for x xs. do 를 쓴다. for 는 다음 원소로 넘어가는 일을 언어가 맡으므로 이 결함이 생길 자리가 없다.
나머지를 받는 자리는 두 가지로 적을 수 있다. case _ 와 마지막 else 는 같은 일을 한다.
examples/ch07/matchelse.low
module matchelse .
rem run: band 1
rem run: band 3
rem run: band 99
fn band input n u64 . output u64 . do
match n. do
case 1 do
return 10 .
end .
case 2 to 5 do
return 20 .
end .
rem 나머지를 `else` 로 받는다 --- `case _ .` 와 같은 자리다
else do
return 99 .
end
end .
end .
실행 결과
$ lowentc --run band matchelse.low 1
band(1) = 10
$ lowentc --run band matchelse.low 3
band(3) = 20
$ lowentc --run band matchelse.low 99
band(99) = 99
else 뒤에는 갈래를 더 둘 수 없다 — 이미 다 받았으므로 그 뒤의 갈래는 한 번도 돌지 않고, E-MATCH-REDUNDANT 로 거절된다.
흔한 오해. match 의 갈래도 C 의 switch 처럼 아래로 흘러간다
examples/ch07/nofall.low
module nofall .
rem run: label 1
rem run: label 2
rem run: label 9
fn label input c u64 . output u64 .
do
var score u64 0 .
match c. do
rem 맞은 갈래 하나만 실행된다 --- 다음 갈래로 흘러가지 않으므로 `break` 가 필요 없다
case 1 do set score. add score. 10 . . end .
case 2 do set score. add score. 20 . . end .
case _ do set score. add score. 1 . . end .
end .
return score. .
end .
실행 결과
$ lowentc --run label nofall.low 1
label(1) = 10
$ lowentc --run label nofall.low 2
label(2) = 20
$ lowentc --run label nofall.low 9
label(9) = 1
맞은 갈래 하나만 실행되고 다음 갈래로 흘러가지 않는다. break 를 적는 버릇도, 빠뜨려서 생기는 결함도 없다. 두 경우에 같은 일을 하려면 갈래를 or 로 묶는다(11장).
7.7 이 장의 문법을 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
if c do … end . | 조건이 참이면 블록 | 조건은 do 에서 끝난다 — 마침표는 문장만 닫는다 |
if c do … end else do … end . | 둘 중 하나 | 여는 말과 닫는 말이 늘 짝을 이룬다 |
… end else if c2 do … end . | 갈래 잇기 | 새 낱말(elif) 없이 있는 낱말을 잇는다 |
while c do … end . | 조건이 참인 동안 | 조건을 바퀴마다 다시 본다 |
repeat i u64 n. do … end . · range i u64 a. b. do … end . | 0 부터 n 번 · a 에서 b 까지 | 수를 세는 일은 머리말이 맡는다 — 셈 변수를 손으로 올리지 않는다 |
cycle i u64 v while c next e do … end . | 다음 값을 직접 적는 되풀이 | 시작 · 조건 · 다음 값이 머리 한 줄에 모인다 |
for x xs. do … end . | 슬라이스의 원소를 차례로 | 다음 원소로 넘어가는 일을 언어가 맡는다 |
break . · continue . | 반복에서 나가기 · 다음 회차로 | 흐름을 바꾸는 문장 |
break 2 . · continue 2 . | 두 겹째 되풀이에서 나가기 · 그 되풀이의 다음 회차로 | 깃발 변수 없이 겹친 되풀이를 벗어난다 |
guard c else return … . | 조건이 아니면 떠난다 — 지난 뒤엔 조건이 사실 | else 가 반드시 떠나야 한다 |
return e . | 값을 돌려주고 끝낸다 | 값을 내는 op 은 모든 길에서 |
match v. do case … do … end . … end . | 경우별로 가르기 | 빠짐없이, 겹침 없이 — 흘러내림이 없다 |
case 1 to 9 · case _ · case y when c | 범위 · 나머지 전부 · 가드가 붙은 갈래 | 정수는 경우가 많아 _ 가 흔히 필요하다 |
panic "…" . | 되돌릴 수 없는 멈춤(효과) | effects panic 을 적는 proc 에서만 |
표 7.1 — 흐름의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
bool 이어야 한다. while 은 조건으로, for 는 슬라이스로 되풀이하고 break·continue 로 흐름을 바꾼다. guard 의 else 는 반드시 떠나고, 지난 뒤에는 조건이 사실이 된다. 값을 돌려주는 op 은 모든 길에서 돌려주어야 한다. match 는 빠짐없이, 겹침 없이 경우를 덮어야 한다. panic 은 효과다.