8 실패와 결함

먼저 알아야 할 것: 3장(서명의 or fail), 6장(push 의 실패).

이 장이 답하는 물음

8.1 두 가지 잘못

Pinax 는 잘못을 둘로 가른다.

예외가 없으므로 잘못이 낱말을 몇 겹 건너뛰어 엉뚱한 곳에서 잡히는 일도 없다.

실패의 까닭은 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 실패를 다루는 세 낱말

실패할 수 있는 낱말 바로 뒤에는 지킴 낱말 하나가 와야 한다.

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 end0실패의 길: 까닭을 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 부터 선언한 차례대로 붙으므로 번호에 기대지 말고 이름을 읽는다.

정리