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_linef 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 에 넘겨야 한다.

토큰그 뒤의 스택일어난 일
markm구역의 지금 자리를 표시로 올린다. 한 번만 쓰는 값이다.
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 값을 전역에 넣어 두면 누가 닫는가?

답. 아무도 닫지 않는다. 전역에 넣는 것도 “넘겨주기” 라서 검사는 거기서 끝나고, 그 뒤는 프로그램의 책임이다. 전역에서 꺼내 닫는 낱말을 따로 두고, 실행이 끝나기 전에 부른다. 결함으로 끝난 실행의 핸들만 처리기가 닫는다.

정리