8 실패와 결함
먼저 알아야 할 것: 3장(서명의 or fail), 6장(push 의 실패).
이 장이 답하는 물음
- 일이 잘못될 때 Pinax 는 무엇을 하는가? 예외가 없는데 어떻게 다루는가?
- 실패를 다루지 않고 지나칠 수 있는가?
- 결함은 무엇이고, 실패와 어떻게 다른가?
8.1 두 가지 잘못
Pinax 는 잘못을 둘로 가른다.
- 실패는 일어날 수 있다고 본 일이다. 수로 읽히지 않는 글, 찬 목록, 없는 키. 실패할 수 있는 낱말은 서명에
or fail이 있고, 부른 쪽이 바로 그 자리에서 다룬다. - 결함은 프로그램이 틀렸다는 뜻이다. 넘침, 0 으로 나눔, 범위 밖의 번호. 실행이 그 자리에서 멈추고 호스트가 받는다. 스크립트 안에서 결함을 잡는 길은 없다.
예외가 없으므로 잘못이 낱말을 몇 겹 건너뛰어 엉뚱한 곳에서 잡히는 일도 없다.
실패의 까닭은 err 타입의 값이다. 이름이 정해진 것은 열둘이고, e err_text 가 그 이름을 글로 준다. 모두 fail 로 낼 수 있고, 처리기가 결함으로 내는 것도 있다.
| 이름 | 언제 |
|---|---|
err.failed | 일반적인 실패. assert 가 거짓이면 이 결함 |
err.not_found | 찾는 것이 없다: 키, 파일, 입력의 끝, str.try_cut 의 구분자 |
err.format | 글이나 바이트의 모양이 틀리다: str.to_int, json, bytes.unpack |
err.full | 용량이 다 찼다: 목록, 큐, 버퍼, 구역 |
err.range | 번호가 범위 밖이다. 빈 목록의 pop, 빈 큐의 take |
err.overflow err.div_zero | 정수가 넘쳤다, 0 으로 나눴다. 결함이지만 chk_ 낱말은 실패로 준다 |
err.depth err.stale | 호출이 너무 깊다, 돌려준 줄이나 핸들을 썼다. 결함 |
err.cancelled err.deadlock err.interrupted | 작업(13장)과 호스트(14장)에 관한 것 |
8.2 실패를 다루는 세 낱말
실패할 수 있는 낱말 바로 뒤에는 지킴 낱말 하나가 와야 한다.
or_fail— 같은 까닭으로 이 낱말도 실패한다. 이 낱말의 서명에도or fail이 있어야 한다.or_return— 이 낱말을 조용히 끝낸다.or_else … end— 대신할 값을 만든다.or_else (e) … end로 까닭을 받는다.
08-parse.pnx
use io
use str
rem Read "name,score" lines; a bad line is reported, not fatal.
word parse (line str gives name str, score int or fail)
line "," str.try_cut or_fail to (n, s) rem no comma: fail the same way
s str.trim str.to_int or_fail to v rem not a number: fail
v 0 lt v 100 gt or if err.range fail end rem our own rule: 0..100
n str.trim v
end
word report (line str)
line parse or_else (e) cat ("skipped \"", line, "\": ", e err_text) io.println return end to (name, score)
cat (name, " scored ", score) io.println
end
text END
kim, 91
lee 77
park, ninety
choi, 140
END
str.lines do (l str) l report end each
출력
$ pnx 08-parse.pnx
kim scored 91
skipped "lee 77": not_found
skipped "park, ninety": format
skipped "choi, 140": range
parse 는 세 곳에서 실패할 수 있고, 모두 or_fail 로 넘긴다. 자기 규칙(0 부터 100)은 err.range fail 로 실패한다. report 는 실패를 or_else (e) 로 받아 까닭과 함께 알리고 그 줄을 건너뛴다.
08-or-return.pnx
use io
list seen int 3
word note_it (n int)
n seen push or_return rem full: quietly do nothing
cat ("noted ", n) io.println
end
1 note_it 2 note_it 3 note_it 4 note_it
출력
$ pnx 08-or-return.pnx
noted 1
noted 2
noted 3
지킴 낱말 뒤의 스택은 성공했을 때와 실패했을 때가 같아야 한다. 실패한 낱말은 결과를 하나도 남기지 않으므로, or_else … end 의 몸이 성공했을 때의 결과와 같은 타입의 값을 만들어 그 자리를 채운다.
08-or-else-stack.pnx
use io
use str
word to_number (s str gives int)
s str.to_int rem on success: one int on the stack
or_else (e) 0 end rem on failure: the block gives one int instead
end rem so after or_else the stack is the same either way
"42" to_number str.to_str io.println
"forty" to_number str.to_str io.println
출력
$ pnx 08-or-else-stack.pnx
42
0
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
s | "forty" | 글을 올린다. |
str.to_int | — | 수로 읽지 못해 실패한다. 결과는 없다. |
or_else (e) 0 end | 0 | 실패의 길: 까닭을 e 에 받고, 몸이 0 을 올린다. 성공했다면 이 몸을 건너뛰고 수가 남는다. |
몸이 다른 타입을 남기면 거절된다. or_else 뒤의 코드는 어느 길로 왔는지 모르기 때문이다(4장의 if 와 같은 규칙).
08-or-else-wrong.pnx
use str
word to_number (s str gives int)
s str.to_int or_else (e) "none" end rem success gives an int, this block a str
end
거절
$ pnx 08-or-else-wrong.pnx
08-or-else-wrong.pnx:3:35: the body of or_else must leave what the word would have given, or leave the word or the loop
08-guard-missing.pnx
use str
word twice (s str gives int) s str.to_int 2 mul end
거절
$ pnx 08-guard-missing.pnx
08-guard-missing.pnx:2:43: a word that can fail is followed by or_fail, or_return or or_else
흔한 오해: 파일의 맨 바깥에서 or_fail 을 쓴다
or fail 서명이 없는 것으로 친다. 그래서 거기서는 or_fail 도 or_return 도 return 도 쓰지 못한다. 일을 낱말로 묶고, 맨 바깥에서는 or_else 로 받는다.08-load-code-or-fail.pnx
use io
use str
"42" str.to_int or_fail str.to_str io.println
거절
$ pnx 08-load-code-or-fail.pnx
08-load-code-or-fail.pnx:3:17: or_fail passes the failure on, so this word needs `or fail` in its signature
8.3 결함
08-fault.pnx
use io
use str
word average (total int, n int gives int) total n div end
cat ("average of 10 over 4: ", 10 4 average) io.println
cat ("average of nothing: ", 0 0 average) io.println
결함으로 멈춤
$ pnx 08-fault.pnx
average of 10 over 4: 2
08-fault.pnx:3: division by zero
total int = 0
n int = 0
pnx 는 결함의 갈래, 줄, 그리고 그 낱말의 지역 변수를 보여 준다. 결함으로 끝난 실행이 구역에 만든 것은 되감기고, 연 파일 같은 핸들은 처리기가 닫는다.
결함이 날 수 있는 자리에서 그것을 실패로 받고 싶으면 chk_ 낱말을 쓴다.
08-chk.pnx
use io
word safe_average (total int, n int gives int or fail) total n chk_div or_fail end
word show () cat ("10/4: ", 10 4 safe_average or_else (e) -1 end, ", 0/0: ", 0 0 safe_average or_else (e) -1 end) io.println end
show
출력
$ pnx 08-chk.pnx
10/4: 2, 0/0: -1
assert 는 거짓이면 결함을 낸다. 부르는 쪽이 지켜야 할 약속을 적을 때 쓴다.
08-assert.pnx
use io
word percent (part int, whole int gives int)
whole 0 gt assert rem a promise the caller must keep
part 100 mul whole div
end
cat ("3 of 4: ", 3 4 percent, "%") io.println
cat ("3 of 0: ", 3 0 percent, "%") io.println
결함으로 멈춤
$ pnx 08-assert.pnx
3 of 4: 75%
08-assert.pnx:3: failed
part int = 3
whole int = 0
문답
물음. 무엇을 실패로, 무엇을 결함으로 할지 어떻게 고르는가?
답. 바깥에서 온 자료 때문에 일어나는 일(사용자의 입력, 파일, 네트워크)은 실패다. 프로그램을 고쳐야만 사라지는 일(잘못 계산한 번호, 지키지 않은 약속)은 결함이다.
문답
물음. 내 낱말이 새 종류의 실패를 알리고 싶다. 이름을 새로 만들 수 있는가?
답. 만들 수 있다. err 이름 은 그 모듈의 새 실패 코드를 선언한다. 열두 이름과 똑같이 fail 로 내고, or_else (e) 로 받고, eq 나 case 로 견주고, err_text 로 이름을 읽는다.
08-own-codes.pnx
use io
use str
err too_short
err too_long
word check_password (p str or fail)
p str.len 8 lt if too_short fail end
p str.len 32 gt if too_long fail end
end
word try (p str)
p check_password or_else (e) cat ("refused: ", e err_text) io.println return end
"accepted" io.println
end
"abc" try
"a password of more than thirty-two bytes" try
"abcdefg8" try
출력
$ pnx 08-own-codes.pnx
refused: too_short
refused: too_long
accepted
까닭이 둘인 실패를 같은 코드 하나로 내면, 받는 쪽은 둘을 가를 수 없다. 둘 다 err.range 인 “너무 짧음”과 “너무 김”이 그렇다. 까닭마다 코드를 하나씩 선언한다. 호스트도 그 이름을 받는다(pinax_error_text). 번호는 0x400 부터 선언한 차례대로 붙으므로 번호에 기대지 말고 이름을 읽는다.
정리
- 실패할 수 있는 낱말 뒤에는
or_failor_returnor_else가 와야 한다(표준 11.2). err 이름은 그 모듈의 실패 코드를 선언하고,err_text는 그 이름을 준다(8.14).or_else의 몸은 성공했을 때의 결과를 대신 남긴다(11.2).or_fail과fail은or fail서명 안에서만, 정의 밖에서는or_else만 쓴다(11.2).- 결함은 실행을 멈추고, 실행이 연 핸들은 처리기가 닫는다(11.3).
- 넘침과 0 으로 나눔을 실패로 받으려면
chk_낱말을 쓴다(15.1).