9 한 번만 쓰는 값과 구역
먼저 알아야 할 것: 5장(구역, mark/release), 7장(풀의 줄), 8장(실패).
이 장이 답하는 물음
- 열린 파일처럼 꼭 닫아야 하는 것을 처리기는 어떻게 지켜 주는가?
- 실패하는 경로에서도 닫았는지 어떻게 아는가?
- 글을 많이 만드는 루프는 구역을 어떻게 아껴 쓰는가?
- 닫는 코드를 떠나는 길마다 되풀이하지 않으려면?
9.1 넘겨주어야 하는 값
file.open_read 의 서명은 (path str gives owned file or fail) 이다. owned 는 이 값이 한 번만 쓰는 값이라는 뜻이다. 받은 쪽은 그 값을 모든 경로에서 정확히 한 번 넘겨줘야 한다: owned 로 받는 낱말(file.close)에 주거나, 전역에 넣거나, owned 결과로 돌려준다. owned 없이 받는 낱말(file.write, file.read_line)에는 몇 번이든 빌려줄 수 있다.
두 가지 쓰임을 표로 견주면 이렇다.
| 빌려주기 | 넘겨주기 | |
|---|---|---|
| 받는 쪽의 서명 | (f file …): owned 가 없다 | (f owned file …), 또는 owned 결과 |
| 몇 번 | 몇 번이든 | 모든 경로에서 정확히 한 번 |
| 넘긴 뒤 | 그대로 쓸 수 있다 | 더는 쓰지 못한다 |
| 예 | f file.write, f file.read_line | f file.close, 전역에 넣기, 돌려주기 |
09-copy-file.pnx
use io
use file
rem Copy a file line by line. Both files are owned: each must be closed on every path.
word copy_lines (source str, target str gives n int or fail)
source file.open_read or_fail to src
target file.open_write or_else (e) src file.close e fail end to dst
0 to n
loop
src file.read_line or_else (e) break end to line
cat (line, "\n") dst file.write or_else (e) src file.close dst file.close e fail end
n 1 add to n
end
src file.close
dst file.close
n
end
word setup (or fail)
"in.txt" file.open_write or_fail to f
"one\ntwo\nthree\n" f file.write or_else (e) f file.close e fail end
f file.close
end
setup or_else (e) end
word main () cat ("copied ", "in.txt" "out.txt" copy_lines or_else (e) -1 end, " lines") io.println end
main
출력
$ pnx 09-copy-file.pnx
copied 3 lines
copy_lines 에는 파일이 둘이고, 떠나는 길이 넷이다. 둘째 파일을 못 열면 첫째를 닫고 실패한다. 쓰다 실패하면 둘 다 닫고 실패한다. 처리기는 이 모든 경로를 적재 때 따라가서, 어느 하나라도 닫지 않으면 거절한다.
09-forgot-close.pnx
use file
word first_line (path str gives str or fail)
path file.open_read or_fail to f
f file.read_line or_fail
end
거절
$ pnx 09-forgot-close.pnx
09-forgot-close.pnx:4:20: an owned value in a local is not given on every path (close, free or store it)
이 낱말은 read_line 이 실패하면 or_fail 로 떠나는데, 그 길에서 f 를 닫지 않는다. 성공하는 길에서도 닫지 않는다. 처리기는 f 를 만든 자리를 가리킨다.
09-close-twice.pnx
use file
word f (path str or fail)
path file.open_read or_fail to h
h file.close
h file.close
end
거절
$ pnx 09-close-twice.pnx
09-close-twice.pnx:5:3: this local gave its owned value away; it holds nothing to read
문답
물음. 결함이 나면 열린 파일은 어떻게 되는가?
답. 결함으로 끝난 실행이 연 핸들은 처리기가 닫는다. 그래서 owned 검사는 결함이 아닌 경로들만 본다.
흔한 오해: dup 으로 하나 더 만들어 두면 편하다
owned 값은 dup 으로 늘리거나 drop 으로 버리지 못한다. 늘리면 두 번 닫게 되고, 버리면 닫지 못한다. 지역 변수에 넣고 이름으로 빌려준다.09-drop-owned.pnx
use file
word glance (path str or fail)
path file.open_read or_fail rem an owned file on the stack
drop rem thrown away: it would never be closed
end
거절
$ pnx 09-drop-owned.pnx
09-drop-owned.pnx:4:3: an owned value is kept in a local or given on at once; this word would lose it
9.2 구역을 아껴 쓰기
cat 은 새 글을 구역에 쓴다. 구역의 크기는 정해져 있으므로, 글을 계속 만드는 루프는 바퀴마다 mark 와 release 로 구역을 되돌린다. 실패해서 떠나는 길에서도 되돌린다 — mark 가 준 것도 한 번만 쓰는 값이기 때문이다.
09-region-loop.pnx
use io
use str
rem Make a long report in a small region: each turn gives its bytes back.
buffer report 4096
word build (or fail)
1 200 range do (i int)
mark to m
cat ("row ", i, ": ", i i mul, "\n") report append or_else (e) m release e fail end
m release
end each
end
build or_else (e) cat ("full: ", e err_text) io.println end
cat ("report: ", report len, " bytes") io.println
출력
$ pnx 09-region-loop.pnx
report: 2735 bytes
한 바퀴를 따라가면 이렇다. mark 가 준 표시도 한 번만 쓰는 값이라, 실패해서 떠나는 길에서도 release 에 넘겨야 한다.
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
mark | m | 구역의 지금 자리를 표시로 올린다. 한 번만 쓰는 값이다. |
to m | — | 이름에 담는다. |
cat (…) | "row 1: 1\n" | 새 글이 구역에 놓인다. |
report append | — | 버퍼에 복사한다. 찼다면 실패한다. |
or_else (e) m release e fail end | — | 실패의 길: 표시를 되돌리고 같은 까닭으로 실패한다. 성공이면 건너뛴다. |
m release | — | 구역을 표시 자리로 되돌린다. cat 이 만든 바이트는 다음 바퀴가 다시 쓴다. |
버퍼에 붙인 바이트는 버퍼에 복사되므로 구역을 되돌려도 남는다.
9.3 미뤄 두기: defer
copy_lines 는 실패하는 자리마다 그때까지 연 것을 모두 닫는 코드를 되풀이했다. defer 코드 end 는 그 코드를 한 번만 적게 한다. 적어 둔 코드는 그 블록(낱말의 몸, 루프의 한 바퀴, do 블록)을 떠나는 모든 길에서 돈다. 끝까지 가도, or_fail 로 떠나도, return 이나 break 로 떠나도 돈다. 여럿이면 적은 차례의 거꾸로 돈다.
09-copy-defer.pnx
use io
use file
rem The same copy with defer: each file is closed once, on every way out.
word copy_lines (source str, target str gives n int or fail)
source file.open_read or_fail to src
defer src file.close end
target file.open_write or_fail to dst
defer dst file.close end
0 to n
loop
src file.read_line or_else (e) break end to line
cat (line, "\n") dst file.write or_fail
n 1 add to n
end
n
end
word setup (or fail)
"in.txt" file.open_write or_fail to f
defer f file.close end
"one\ntwo\nthree\n" f file.write or_fail
end
setup or_else (e) end
word main () cat ("copied ", "in.txt" "out.txt" copy_lines or_else (e) -1 end, " lines") io.println end
main
출력
$ pnx 09-copy-defer.pnx
copied 3 lines
defer src file.close end 뒤로 src 는 defer 가 맡는다. 몸에서는 src file.read_line 처럼 빌려 쓰기만 하고, 닫는 일은 떠날 때 defer 가 한다. 그래서 or_else 의 닫는 코드가 사라지고 or_fail 을 그대로 쓸 수 있다. 구역의 표시도 같다.
09-region-defer.pnx
use io
use str
buffer report 4096
word build (or fail)
1 200 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 09-region-defer.pnx
report: 2735 bytes
흔한 오해: defer 를 적고도 마지막에 직접 닫는다
defer 가 맡은 값을 몸에서 넘겨주면 거절된다. 그대로 두면 떠날 때 두 번 닫히기 때문이다. 닫는 일은 defer 에 맡긴다.09-defer-close.pnx
use file
word first_line (path str gives str or fail)
path file.open_read or_fail to f
defer f file.close end
f file.read_line or_fail
f file.close
end
거절
$ pnx 09-defer-close.pnx
09-defer-close.pnx:6:5: a defer gives this owned value when the block is left; here it can only be lent
defer 는 낱말의 몸, 루프의 몸, do 블록에 바로 쓴다. if 의 가지 안에는 쓰지 못한다. 미룬 코드는 스택을 그대로 두어야 하고, 그 안에서 실패하는 낱말은 or_else 로 다룬다(표준 10.7).
문답
물음. owned 값을 전역에 넣어 두면 누가 닫는가?
답. 아무도 닫지 않는다. 전역에 넣는 것도 “넘겨주기” 라서 검사는 거기서 끝나고, 그 뒤는 프로그램의 책임이다. 전역에서 꺼내 닫는 낱말을 따로 두고, 실행이 끝나기 전에 부른다. 결함으로 끝난 실행의 핸들만 처리기가 닫는다.
정리
owned값은 모든 경로에서 정확히 한 번 넘겨준다. 빌려주기는 몇 번이든 된다(표준 12.4).- 실패의 경로(
or_failor_return)와return에서도 넘겨줘야 한다(11.2). defer 코드 end는 블록을 떠나는 모든 길에서 돈다. 맡은 값은 몸에서 빌려 쓰기만 한다(10.7).owned값은dupdrop하지 못한다(12.4).- 결함이면 처리기가 핸들을 닫는다(11.3).
- 루프의 글은
mark/release로 구역을 되돌린다(12.2).