10 제어
구문
control = "if" { body } [ "else" { body } ] "end"
| "loop" [ "(" NAME ")" ] { body } "end"
| "case" { branch } [ "otherwise" { body } "end" ] "end" ;
branch = values "of" { body } "end" ;
values = case-value | "(" case-value { "," case-value } ")" ;
leave = ( "while" | "until" | "break" ) [ "(" NAME ")" ] | "return" ;
block = "do" [ "(" effect ")" ] { body } "end" ;(1) end 는 가장 안쪽에 열린 것 하나를 닫는다. 여는 낱말은 word test record do if loop case of otherwise or_else defer 다. (텍스트 리터럴과 note 주석은 자기 꼬리표로 닫는다, 6.3, 6.5.)
(2) 처리기는 적재 때 모든 갈래의 스택을 견준다. 갈래가 다시 만나는 자리에서 스택은 어느 갈래로 왔든 같아야 한다. 떠나는 갈래(leaving branch) — return fail break 로 끝나거나 끝나지 않는 루프로 끝나는 갈래 — 는 견주지 않는다.
10.1 갈림
(1) 조건 if 몸 end 는 맨 위의 bool 을 내려 참이면 몸을 돈다. 조건 if 몸 else 몸 end 는 거짓이면 else 의 몸을 돈다.
(2) else 가 없는 if 의 몸은 스택을 들어올 때와 같게 둬야 한다. else 가 있으면 두 몸은 같은 스택을 남겨야 한다.
10-if.pnx
use io
word sign (n int gives str) n 0 lt if "negative" else n 0 eq if "zero" else "positive" end end end
-5 sign io.println
0 sign io.println
word warn (n int) n 100 gt if "big" io.println end end rem no else: the branch leaves the stack as it was
500 warn
출력
$ pnx 10-if.pnx
negative
zero
big
제약
(3) 두 갈래가 남기는 스택이 다르면 거절된다.
10-if-unequal.pnx
word f (n int gives int) n 0 lt if 1 2 else 3 end end
거절
$ pnx 10-if-unequal.pnx
10-if-unequal.pnx:1:47: the two branches of this if leave different stacks
10.2 루프
(1) loop 몸 end 는 몸을 되풀이한다. 몸 안에서 다음 낱말이 루프를 떠난다.
조건 while— 조건이 거짓이면 떠난다.조건 until— 조건이 참이면 떠난다.break— 조건 없이 떠난다.
셋은 몇 번이든 쓸 수 있고, 루프의 몸에 바로 쓰거나 그 안의 갈래(if else case or_else) 안에 쓴다. 가장 안쪽 루프를 떠난다.
(2) 한 바퀴는 스택을 들어올 때와 같게 둬야 한다. 떠나는 자리마다 스택은 루프에 들어올 때와 같아야 한다. 루프의 결과가 필요하면 지역 변수에 둔다.
10-loop.pnx
use io
use str
word digits (n int gives int) rem how many decimal digits
1 to count_
loop
n 10 div to n
n 0 eq until
count_ 1 add to count_
end
count_
end
12345 digits str.to_str io.println
word first_square_over (limit int gives int)
1 to i
loop
i i mul limit gt if break end
i 1 add to i
end
i
end
50 first_square_over str.to_str io.println
출력
$ pnx 10-loop.pnx
5
8
(3) loop (이름) 은 루프에 이름을 붙인다. while (이름) until (이름) break (이름) 은 그 이름의 루프까지 한 번에 떠난다. 이름은 그 루프의 몸 안에서만 보이고 값이 아니다. 가림 금지(9.5)를 따른다.
10-loop-named.pnx
use io
use str
word find_pair (target int gives str) rem the first a, b in 1..9 with a*b = target
1 to a
0 to b
loop (rows)
1 to b
loop
a b mul target eq until (rows) rem leaves both loops at once
b 1 add to b
b 9 gt until
end
a 1 add to a
a 9 gt if "none" return end
end
cat (a, " x ", b)
end
42 find_pair io.println
출력
$ pnx 10-loop-named.pnx
6 x 7
(4) 자기를 떠나는 while until break 가 하나도 없는 루프는 끝나지 않는 루프다. 끝나지 않는 루프는 떠나는 갈래로 센다. 그래서 그 뒤에 결과를 적지 않아도 낱말의 서명이 맞는다. 안쪽 루프의 until 은 바깥 루프를 떠나는 것으로 세지 않지만, 안쪽에 쓴 until (바깥 이름) 은 센다.
10-forever.pnx
use io
use str
word first_multiple (n int, k int gives int) rem a loop with no exit counts as leaving
loop
n k mod 0 eq if n return end
n 1 add to n
end
end
100 7 first_multiple str.to_str io.println
출력
$ pnx 10-forever.pnx
105
제약
(5) 한 바퀴가 스택을 바꾸면 거절된다.
10-loop-net.pnx
word f (gives int)
0
loop
1 add
dup 10 gt until
1
end
end
거절
$ pnx 10-loop-net.pnx
10-loop-net.pnx:7:3: one turn of a loop must leave the stack as it found it
(6) while until break 를 루프 밖에 쓰면 거절된다. do 블록 안에서 바깥 루프를 떠나지도 못한다. each, each_index, times 의 블록 안에서는 그 블록을 떠난다(10.6 (4)).
10-while-outside.pnx
word f (n int) n 0 gt while end
거절
$ pnx 10-while-outside.pnx
10-while-outside.pnx:1:23: while, until and break belong inside a loop
(7) 떠나는 낱말(return fail break)이나 끝나지 않는 루프 바로 뒤에 같은 갈래의 코드가 더 있으면 닿지 않는 코드로 거절된다.
10-unreachable.pnx
word f (n int gives int) n return 1 add end
거절
$ pnx 10-unreachable.pnx
10-unreachable.pnx:1:35: this code cannot be reached: the code before it always leaves
10.3 고르기
(1) 값 case 가지… end 는 맨 위의 int, 좁은 정수(7.6), err 나 str 를 내려 가지의 값과 견주고, 같은 첫 가지의 몸을 돈다. 가지의 값은 리터럴, 이 소스의 const, 오류 코드(err.range), 또는 다른 모듈이 내보낸 상수나 실패 코드(모듈.이름, 8.14)다. 한 가지에 값이 여럿이면 괄호에 쉼표로 적는다. 맞는 가지가 없으면 otherwise 의 몸을 돈다. 가지마다 같은 스택을 남겨야 한다.
10-case.pnx
use io
const quit_code 9
word describe (cmd str gives str)
cmd case
"add" of "adds a line" end
("rm", "del") of "deletes a line" end
otherwise "unknown command" end
end
end
word level (n int gives str)
n case
0 of "off" end
1 of "low" end
quit_code of "quit" end
otherwise "high" end
end
end
"del" describe io.println
"mv" describe io.println
9 level io.println
출력
$ pnx 10-case.pnx
deletes a line
unknown command
quit
제약
(2) 같은 값이 두 가지에 있으면 거절된다.
10-case-twice.pnx
word f (n int gives int) n case 1 of 10 end 1 of 20 end otherwise 0 end end end
거절
$ pnx 10-case-twice.pnx
10-case-twice.pnx:1:45: this value is already in an earlier branch of the case
(3) 모듈의 상수(json.key, err.full 같은 경로)는 가지의 값이 되지 못한다. 그런 값은 eq 로 견준다.
10-case-path.pnx
use json
word f (k int gives bool) k case json.key of true end otherwise false end end end
거절
$ pnx 10-case-path.pnx
10-case-path.pnx:2:34: a case value is a literal or the name of a const
결함
(4) otherwise 가 없고 맞는 가지도 없으면 결함 range 다.
10-case-nomatch.pnx
use io
word f (n int gives str) n case 1 of "one" end 2 of "two" end end end
2 f io.println
3 f io.println
결함으로 멈춤
$ pnx 10-case-nomatch.pnx
two
10-case-nomatch.pnx:2: index out of range
n int = 3
10.4 돌아가기
(1) return 은 낱말을 그 자리에서 끝낸다. 그 자리의 스택은 결과와 맞아야 한다(9.2 (4)).
제약
(2) return 은 정의(낱말, 값이 되는 블록, 시험) 안에서만 쓴다. 정의 밖의 코드에 쓰면 거절된다.
10-return-outside.pnx
use io
"start" io.println
return
거절
$ pnx 10-return-outside.pnx
10-return-outside.pnx:3:1: return belongs inside a definition
10.5 블록
(1) do … end 는 블록(block)이다. 블록은 두 가지로 쓰인다. 모양은 같고, 블록을 받는 쪽이 가른다.
- 끼워 넣는 블록(inline block) — 조합 낱말(10.6)에 바로 준 블록. 그 자리에 펼쳐진다. 그래서 바깥의 지역 변수를 읽고 쓸 수 있고, 안에서
return하면 바깥 낱말을 떠난다. - 값이 되는 블록(block value) — 지역 변수에 넣거나, 블록을 받는 매개변수(
word (서명)타입)에 주는 블록. 이름 없는 낱말 하나가 되며call로 부른다. 바깥의 지역 변수를 보지 못한다(붙잡지 않는다). 전역은 본다.
(2) do 바로 뒤에 서명을 쓸 수 있고, 그 이름들은 낱말 정의와 같이 블록의 지역 변수가 된다. 받는 쪽이 효과를 정해 주면 서명을 줄일 수 있다.
10-inline-locals.pnx
use io
use str
word total_over (limit int gives int)
0 to acc
1 10 range do (i int) i limit gt if acc i add to acc end end each rem the block sees acc and limit
acc
end
5 total_over str.to_str io.println
출력
$ pnx 10-inline-locals.pnx
30
10-value-block.pnx
use io
use str
word twice (n int, f word (int gives int) gives int) n f call f call end
5 do (x int gives int) x 3 mul end twice str.to_str io.println
word add_one (int gives int) 1 add end
word demo (gives int)
do (x int gives int) x add_one end to step rem a block value in a local
41 step call
end
demo str.to_str io.println
출력
$ pnx 10-value-block.pnx
45
42
(2a) 서명이 없는 값이 되는 블록의 몸이 word 로 정의한 낱말(이 소스의 것이든 쓰는 모듈이 내보낸 것이든) 하나 뿐이면(do by_len end, do geo.area end), 그 값은 그 낱말 자체다. 타입은 그 낱말의 서명을 가진 word 이고, call 은 그 낱말을 부른다. 다른 낱말은 블록의 서명을 쓴 블록으로 준다. 타입만 쓴 입력은 스택에 남으므로(9.2 (2)) 블록의 몸은 그 낱말 하나다: do (str, str gives bool) str.starts_with end.
10-word-value.pnx
use io
use str
word by_len (a str, b str gives bool) a str.len b str.len lt end
word shorter (f word (str, str gives bool), a str, b str gives str) a b f call if a else b end end
word demo ()
do by_len end to order rem the word by_len itself: word (str, str gives bool)
cat ("shorter: ", order "pear" "fig" shorter) io.println
do (str, str gives bool) str.starts_with end to starts rem a word of the language, with its signature
"banana" "ban" starts call if "starts with ban" io.println end
end
demo
출력
$ pnx 10-word-value.pnx
shorter: fig
starts with ban
제약
(3) 값이 되는 블록이 바깥의 지역 변수를 쓰면 거절된다(그 이름은 블록 안에 선언돼 있지 않다).
10-value-block-outer.pnx
word twice (n int, f word (int gives int) gives int) n f call f call end
word scale (k int gives int) 5 do (x int gives int) x k mul end twice end
거절
$ pnx 10-value-block-outer.pnx
10-value-block-outer.pnx:2:55: this name is not declared before this place
(4) 루프로 펼쳐지는 블록(each times 등)은 스택을 들어올 때와 같게 둬야 한다.
10-block-net.pnx
array xs int 3
word f (gives int) 0 xs do (x int) x end each end
거절
$ pnx 10-block-net.pnx
10-block-net.pnx:2:38: this block must leave the stack as it found it
(4a) 서명이 없는 값이 되는 블록은 아무것도 받지 않는다. 그 안에서 값이 필요한 낱말은 거절되고, 메시지가 (2a)의 두 꼴을 보인다.
10-word-value-bare.pnx
use str
word demo ()
do str.starts_with end drop
end
거절
$ pnx 10-word-value-bare.pnx
10-word-value-bare.pnx:3:6: a block without a signature takes nothing; a word of the program is given as do NAME end, any other word with the block's signature: do (str, str gives bool) str.starts_with end
(4b) forward 로 선언한 낱말은 정의된 뒤에만 값으로 준다.
10-word-value-forward.pnx
forward word later (n int)
word demo ()
do later end drop
end
word later (n int) end
거절
$ pnx 10-word-value-forward.pnx
10-word-value-forward.pnx:3:6: a word declared with forward is given as a value only after its definition
10.6 줄기와 조합 낱말
(1) 줄기(stem)는 차례로 값을 내는 것이다. 줄기는 값이 아니어서 지역 변수에 넣지 못하고, 바로 뒤의 조합 낱말이 받아야 한다. 둘이 합쳐 루프 하나가 된다. 줄기를 내는 것은 다음과 같다.
| 줄기 | 원소 |
| 배열, 목록의 이름 | 칸의 값(레코드면 줄). 목록은 길이까지. 큐, 버퍼, 찾아보기는 줄기가 아니다 |
| 풀의 이름 | 살아 있는 줄, 칸의 차례로. each 에만(8.9 (3a)) |
lo hi range | lo 부터 hi 앞까지의 int |
s sep str.split, s str.lines, s str.words, s str.chars | str 뷰(16.1). chars 는 UTF-8 글자 하나씩 |
s str.bytes | u8: 바이트(16.1) |
s pat ptn.matches, s re rex.matches, s re rex.split | 맞은 부분(16.6, 16.7) |
s csv.fields | 필드(16.9) |
(2) 조합 낱말은 다음과 같다. “블록” 열은 블록이 받고 주는 것이다.
| 낱말 | 블록 | 결과 |
each | (T) | 없음 |
each_index | (T, i int) | 없음 |
fold | (acc U, T gives U) | U. 첫 값을 줄기 앞에 둔다: 0 xs do … end fold |
count | (T gives bool) | int |
any all | (T gives bool) | bool. 답이 정해지면 일찍 끝난다 |
first | (T gives bool) | T or fail. 없으면 err.not_found |
sum | 없음 | int(dec(N) 의 줄기면 dec(N)). 넘치면 결함 overflow |
minimum maximum | 없음 | int or fail(dec(N) 도 같다). 빈 줄기면 err.not_found |
map_into | (T gives U) | 뒤에 목적지. or fail |
filter_into | (T gives bool) | 뒤에 목적지. or fail |
n times | () | 없음. 줄기 없이 n 번 |
10-combinators.pnx
use io
use str
array scores int 6
assign scores (72, 95, 88, 61, 90, 79)
scores sum str.to_str io.println
scores do (s int gives bool) s 90 ge end count str.to_str io.println
scores do (s int gives bool) s 60 lt end any if "someone failed" else "all passed" end io.println
0 scores do (acc int, s int gives int) acc s max end fold str.to_str io.println
scores do (s int, i int) i 2 lt if cat ("#", i, " ", s) io.println end end each_index
1 5 range do (i int) i str.to_str io.print end each
"" io.println
3 do "x" io.print end times
"" io.println
출력
$ pnx 10-combinators.pnx
485
2
all passed
95
#0 72
#1 95
1234
xxx
10-first.pnx
use io
use str
array xs int 5
assign xs (3, 8, 12, 5, 20)
xs do (x int gives bool) x 10 gt end first or_else (e) -1 end str.to_str io.println
xs do (x int gives bool) x 100 gt end first or_else (e) -1 end str.to_str io.println
xs minimum or_else (e) 0 end str.to_str io.println
출력
$ pnx 10-first.pnx
12
-1
3
(3) map_into 와 filter_into 는 새 저장소를 만들지 않고 목적지에 쓴다. 목적지가 목록이면 끝에 push 하고 아무것도 주지 않는다. 목적지가 배열이면 0 번 칸부터 쓰고 쓴 칸의 수를 준다(gives n int or). 목적지가 차면
failerr.full 로 실패하고, 그때까지 쓴 것은 남는다. 목적지는 모음 매개변수여도 된다(9.9).
10-map-into.pnx
use io
use str
array xs int 4
assign xs (1, 2, 3, 4)
list squares int 4
array evens int 4
word go (or fail)
xs do (x int gives int) x x mul end squares map_into or_fail
xs do (x int gives bool) x 2 mod 0 eq end evens filter_into or_fail to n
cat ("squares ", 3 squares idx, ", evens written ", n) io.println
end
go or_else (e) e err_text io.println end
출력
$ pnx 10-map-into.pnx
squares 16, evens written 2
(4) 줄기와 조합 낱말은 루프 하나를 이루고, each, each_index, times 의 블록 안에서 break 는 그 루프를 떠난다. 줄기는 멈추고 조합 낱말 뒤의 코드가 돈다. while 과 until 은 루프를 떠나듯 떠난다(10.2 (1)). 루프를 떠나는 규칙이 그대로다: 스택은 블록이 시작할 때와 같고(10.2 (2)), 블록의 미룬 코드가 돈다(10.7). 블록 안의 loop 에 쓴 break 는 그 루프를 떠난다. 모든 원소로 결과를 만드는 다른 조합 낱말의 블록은 일찍 떠나지 못하고, 블록 밖의 이름 붙은 루프는 블록 안에서 떠나지 못한다.
10-leave-each.pnx
use io
use str
word until_blank (s str)
s str.lines do (line str)
line str.len 0 eq if break end
cat ("> ", line) io.println
end each
"done" io.println
end
"first\nsecond\n\nnot read" until_blank
출력
$ pnx 10-leave-each.pnx
> first
> second
done
10.7 미룬 코드
구문
defer = "defer" { body } "end" ;(1) defer 코드 end 는 둘러싼 블록을 떠날 때 돌 코드를 적는다. 블록은 낱말의 몸, 루프의 몸(한 바퀴마다), 조합 낱말의 do 블록(한 번 돌 때마다)이다. 그 블록을 떠나는 모든 길에서 돈다: 블록의 end, 루프의 한 바퀴 끝, break, 루프를 떠나는 while·until, return, or_return, or_fail, fail. 여럿이면 적은 차례의 거꾸로 돈다. 결함으로 끝날 때는 돌지 않는다(그때는 상태가 핸들을 닫는다, 19.6).
10-defer.pnx
use io
use str
buffer trace 256
word say (s str) s trace append or_else (e) end end
rem Every way out of a block runs its defers, the last written first.
word step (n int or fail)
"open " say
defer "close " say end
defer "flush " say end
n 0 lt if err.range fail end
"work " say
end
1 step or_else (e) end
-1 step or_else (e) "(failed) " say end
trace contents str.trim io.println
출력
$ pnx 10-defer.pnx
open work flush close open flush close (failed)
(2) 미룬 코드가 소유 값을 소비하면(m release, f file.close) 그 값은 미룬 코드가 맡는다. 블록을 떠나는 모든 길에서 소비된 것으로 치므로 실패 경로마다 닫는 코드를 따로 쓸 일이 없다. 블록 안에서는 그 값을 빌려 쓸 수 있다.
10-defer-owned.pnx
use io
use str
buffer report 4096
rem A mark taken back at the end of every turn, also when append fails.
word build (or fail)
1 50 range do (i int)
mark to m
defer m release end
cat ("row ", i, ": ", i i mul, "\n") report append or_fail
end each
end
build or_else (e) cat ("full: ", e err_text) io.println end
cat ("report: ", report len, " bytes") io.println
출력
$ pnx 10-defer-owned.pnx
report: 585 bytes
제약
(3) defer 는 낱말의 몸, 루프의 몸, do 블록에 바로 쓴다. if·case·or_else 의 가지 안이나 정의 밖에 쓰면 거절된다.
10-defer-branch.pnx
word f (c bool)
c if defer end end
end
거절
$ pnx 10-defer-branch.pnx
10-defer-branch.pnx:2:8: defer is written right in a word's body, a loop's body or a do block, not in a branch
(4) 미룬 코드는 스택을 받은 그대로 두고 아무것도 남기지 않는다. return fail break while until to set loop do case defer 와 or_fail or_return 을 쓸 수 없다. 실패하는 낱말은 그 안에서 or_else 로 다룬다.
(5) 미룬 코드가 맡은 소유 값을 블록 안에서 소비하면 거절된다.
10-defer-taken.pnx
word twice (gives int)
mark to m
defer m release end
m release
0
end
거절
$ pnx 10-defer-taken.pnx
10-defer-taken.pnx:4:5: a defer gives this owned value when the block is left; here it can only be lent
진단
| 조건 | 메시지 |
|---|---|
| 두 갈래의 스택이 다름 | the two branches of this if leave different stacks |
| 한 바퀴가 스택을 바꿈 | one turn of a loop must leave the stack as it found it |
루프 밖의 while until break | while, until and break belong inside a loop |
결과를 주는 낱말의 블록 안의 while until break | only the block of each, each_index or times is left early; this word gives a result made from every element |
do 블록 밖 루프로의 break (NAME) | a named loop outside this block is not left from inside it; break leaves the block |
| 닿지 않는 코드 | this code cannot be reached: the code before it always leaves |
case 의 같은 값 | this value is already in an earlier branch of the case |
| 펼쳐지는 블록이 스택을 바꿈 | this block must leave the stack as it found it |
정의 밖의 return | return belongs inside a definition |
| 상수가 아닌 가지의 값 | a case value is a literal or the name of a const |
| 줄기가 아닌 것에 조합 낱말 | only an array, a list or a pool is a stem (not a queue, a buffer or an index) |
가지 안의 defer | defer is written right in a word's body, a loop's body or a do block, not in a branch |
정의 밖의 defer | defer belongs inside a definition |
| 미룬 코드가 스택을 바꿈 | a defer leaves the stack as it found it |
| 미룬 코드 안의 떠나는 낱말 | a defer holds no return, fail, break, while, until, to, set, loop, do, case or defer |
미룬 코드 안의 or_fail or_return | a defer does not leave the word; a failure in it is taken with or_else |
| 미룬 코드가 맡은 값을 소비 | a defer gives this owned value when the block is left; here it can only be lent |
| 서명이 없는 값이 되는 블록에서 값이 모자람 | a block without a signature takes nothing; a word of the program is given as do NAME end, any other word with the block's signature: do (str, str gives bool) str.starts_with end |
| 정의 전의 forward 낱말을 값으로 | a word declared with forward is given as a value only after its definition |
10.8 문장의 끝
(1) 코드에 쓴 쉼표나 점(서명, cat, 이름 목록의 괄호 안이 아닌 곳)은 문장(statement)을 끝낸다: 마지막 쉼표나 점 뒤의 코드다. 둘 다 스택을 바꾸지 않고, 둘 다 꼭 써야 하는 것은 아니다. 둘이 없는 코드는 전과 같은 뜻이다.
(2) 쉼표는 아무것도 기다리지 않는다: 다음 문장이 시작할 때 이 문장의 효과가 아직 진행 중일 수 있다.
(3) 점은 장벽(barrier)이다: 점 앞에서 시작한 모든 효과가 점 뒤의 무엇보다 먼저 끝난다. 처리기의 몫은 호스트가 pinax_on_barrier(19.4 (3))로 준 함수를 부르는 것이고, 호스트의 몫은 호스트 낱말이 붙들고 있는 것(표준 출력, 파일에 쓴 것)을 내보내고 뒤에서 시작한 일을 기다리는 것이다. 적재한 코드의 끝과 세션 줄의 끝은 점이다. 결함으로 끝나도 그렇다. 데이터로 적재한 소스에서 둘은 문장을 나눌 뿐 아무것도 부르지 않는다.
(4) 문장이 결과를 써야 하는 효과(읽기, 명령의 출력)는 닿은 자리에서 끝낸다. 두 점 사이에서 같은 자원에 대한 효과는 프로그램의 순서를 지키고, 다른 자원에 대한 효과는 겹칠 수 있다. 이 판은 모든 효과를 프로그램의 순서대로 돌린다. 겹침은 뒤의 판이 쓸 수 있는 여지다.
10-statements.pnx
use io
word show (n int)
cat ("square: ", n n mul) io.println .
end
"start" io.println ,
3 show ,
4 show .
출력
$ pnx 10-statements.pnx
start
square: 9
square: 16