3 낱말과 서명
먼저 알아야 할 것: 1장(낱말 만들기), 2장(스택).
이 장이 답하는 물음
- 서명에는 무엇을 적고, 처리기는 그것으로 무엇을 검사하는가?
- 입력에 이름을 붙이는 것과 붙이지 않는 것은 어떻게 다른가?
- 결과가 둘인 낱말, 자기를 부르는 낱말은 어떻게 쓰는가?
3.1 서명이 하는 일
모든 낱말에는 서명이 있다. 입력, gives, 결과, 그리고 실패할 수 있으면 or fail 을 이 차례로 적는다. 처리기는 서명을 두 쪽에서 검사한다. 부르는 쪽에서는 스택의 맨 위 값들이 입력의 타입과 맞는지, 몸 쪽에서는 몸이 끝날 때 스택이 결과와 정확히 같은지 본다.
03-temperature.pnx
use io
use str
rem Celsius and Fahrenheit, in tenths of a degree so that everything stays an int.
word c_to_f (tenths int gives int) tenths 9 mul 5 div 320 add end
word f_to_c (tenths int gives int) tenths 320 sub 5 mul 9 div end
word show (tenths int gives str) rem 365 -> "36.5"
tenths 10 div to whole
tenths 10 mod abs to part
cat (whole, ".", part)
end
cat ("36.5 C = ", 365 c_to_f show, " F") io.println
cat ("98.6 F = ", 986 f_to_c show, " C") io.println
출력
$ pnx 03-temperature.pnx
36.5 C = 97.7 F
98.6 F = 37.0 C
show 는 int 하나를 받아 str 하나를 준다. 그래서 365 c_to_f show 가 cat 의 항목 하나로 들어갈 수 있다. 온도를 int 의 0.1 도 단위로 다루는 것은 이 판에 실수가 없기 때문이다.
서명은 그 낱말이 스택에 하는 일을 한 줄로 적은 것이다. (tenths int gives int) 는 “맨 위의 int 하나를 가져가고 int 하나를 놓는다” 는 뜻이다. 그래서 낱말의 몸을 읽지 않고도 부르는 쪽의 스택을 따라갈 수 있다.
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
365 | 365 | 값을 올린다. |
c_to_f | 977 | 서명대로 int 하나를 가져가고 int 하나를 놓는다. |
show | "97.7" | int 하나를 가져가고 str 하나를 놓는다. |
3.2 부르면 일어나는 일
낱말을 부르면 처리기는 서명의 입력만큼 스택의 맨 위에서 값을 가져가 낱말에 준다. 먼저 올린 값이 첫 입력이다. 낱말의 몸은 빈 스택에서 시작한다 — 부르는 쪽의 스택 아래쪽 값들은 보이지도 않고 건드릴 수도 없다. 몸이 끝나면 스택에는 서명의 결과만 정확히 남아 있어야 하고, 그 값들이 부르는 쪽의 스택에 올라간다.
03-arg-order.pnx
use io
use str
word minus (a int, b int gives int) a b sub end rem a - b
10 3 minus str.to_str io.println rem 10 was pushed first, so a = 10 and b = 3: 7
3 10 minus str.to_str io.println rem the other way round: -7
word half_of_sum (a int, b int gives int)
a b add rem the body starts with an empty stack
2 div rem and must end holding one int
end
4 8 half_of_sum str.to_str io.println rem (4 + 8) / 2 = 6
출력
$ pnx 03-arg-order.pnx
7
-7
6
10 3 minus 에서 10 이 먼저 올라갔으므로 a 가 10, b 가 3 이다. 빼기의 “먼저 올린 값이 왼쪽” 규칙(2장)과 같다. half_of_sum 의 몸을 따라가 보자. 부르는 쪽의 스택에는 4 8 이 있었지만, 몸 안에서는 그것이 a 와 b 라는 이름이 되고 스택은 비어서 시작한다.
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
| (몸의 시작) | — | a 는 4, b 는 8. 스택은 비었다. |
a b | 4 8 | 이름을 쓰면 그 값이 올라간다. |
add | 12 | 더한다. |
2 div | 6 | 2 로 나눈다. 몸이 끝날 때 int 하나 — 서명과 같다. |
3.3 이름 있는 입력과 없는 입력
입력에 이름을 붙이면(a int) 그 값은 그 이름의 지역 변수가 되고 몸은 빈 스택에서 시작한다. 타입만 쓰면(int) 값이 스택에 남은 채 몸이 시작한다. 한 서명에서 둘을 섞지는 못한다.
03-two-results.pnx
use io
word divmod (a int, b int gives q int, r int) a b div a b mod end rem two results
word bare_square (int gives int) dup mul end rem a bare input stays on the stack
word show () 17 5 divmod to (q, r) cat ("17 = 5 * ", q, " + ", r) io.println end
show
cat ("7 squared is ", 7 bare_square) io.println
출력
$ pnx 03-two-results.pnx
17 = 5 * 3 + 2
7 squared is 49
divmod 는 결과가 둘이다. 결과 쪽의 q r 은 읽는 사람을 위한 이름일 뿐, 지역 변수를 만들지 않는다. 받는 쪽은 to (q, r) 로 둘을 한 번에 이름에 넣는다. 오른쪽 이름이 맨 위 값을 받는다 — 서명에 적힌 차례 그대로다.
bare_square 는 입력에 이름이 없다. 그러면 값이 이름이 되지 않고 스택에 남은 채 몸이 시작한다. 7 bare_square 의 몸은 7 한 개가 있는 스택에서 시작하고, dup mul 이 그것을 제곱한다.
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
| (몸의 시작) | 7 | 이름 없는 입력은 스택에 그대로 있다. |
dup | 7 7 | 하나 더 만든다. |
mul | 49 | 곱한다. 결과 하나가 남는다. |
한 서명에서 이름 있는 입력과 이름 없는 입력을 섞지는 못한다. 섞으면 어느 값이 스택에 남는지 헷갈리기 때문이다.
03-mixed-inputs.pnx
word scale (n int, int gives int) n mul end rem one input named, the other bare
거절
$ pnx 03-mixed-inputs.pnx
03-mixed-inputs.pnx:1:33: inputs are all named or all bare types, not a mix
문답
물음. 입력이 셋 이상이면 어느 쪽이 좋은가?
답. 이름을 붙인다. 값이 둘이면 dup swap 으로 다룰 만하지만, 셋부터는 스택을 머릿속으로 따라가기 어렵다. Pinax 에 rot 이나 pick 이 없는 것도 그래서다.
3.4 서명과 맞지 않는 프로그램
03-result-wrong.pnx
word area (w int, h int gives int) w h mul w end
거절
$ pnx 03-result-wrong.pnx
03-result-wrong.pnx:1:46: the stack does not hold exactly the results of the signature here
몸이 끝날 때 스택에 값이 둘(w h mul 의 결과와 w)인데 서명은 하나를 약속했다.
03-call-wrong.pnx
use io
word greet (name str) cat ("Hi ", name) io.println end
42 greet
거절
$ pnx 03-call-wrong.pnx
03-call-wrong.pnx:3:4: an argument has another type than the word's signature asks for
greet 는 str 를 받는데 42 를 주었다. 이 검사는 돌기 전에 일어나므로, 이런 실수는 그 줄이 실제로 도는지와 상관없이 언제나 드러난다.
결과가 없는 낱말(gives 가 없는 서명)은 몸이 끝날 때 스택이 비어 있어야 한다. 계산한 값을 돌려주려면 서명에 결과를 적는다: (n int gives int).
03-no-result.pnx
use io
word double (n int) n 2 mul end rem the signature gives nothing, yet a value is left
거절
$ pnx 03-no-result.pnx
03-no-result.pnx:2:29: the stack does not hold exactly the results of the signature here
흔한 오해: 핵 낱말의 이름을 변수 이름으로 쓴다
sum count first add 처럼 흔한 낱말은 모두 핵 낱말의 이름이다. Pinax 는 보이는 이름을 다른 것으로 가리지 못하게 하므로(가림 금지), 그런 이름의 매개변수나 지역 변수는 거절된다.03-taken-name.pnx
word average (sum int, count int gives int) sum count div end
거절
$ pnx 03-taken-name.pnx
03-taken-name.pnx:1:45: sum is a word of the language; choose another name
total, n, items 처럼 다른 이름을 쓴다. of end text 같은 예약 낱말도 이름이 되지 못한다.3.5 자기를 부르는 낱말
03-recursion.pnx
use io
word gcd (a int, b int gives int) b 0 eq if a else b a b mod gcd end end rem a tail call
word fib (n int gives int) n 2 lt if n else n 1 sub fib n 2 sub fib add end end
cat ("gcd(1071, 462) = ", 1071 462 gcd) io.println
cat ("fib(20) = ", 20 fib) io.println
출력
$ pnx 03-recursion.pnx
gcd(1071, 462) = 21
fib(20) = 6765
낱말은 미리 알리지 않고도 자기를 부를 수 있다. gcd 처럼 마지막 일이 부르기인 꼬리 부름은 자리를 다시 써서 깊이가 자라지 않는다. fib 처럼 부른 뒤에 일이 남는 부르기는 깊이가 쌓이고, 너무 깊어지면 결함 depth 로 멈춘다. 서로를 부르는 두 낱말은 forward word 로 하나를 먼저 알린다.
문답
물음. 낱말의 몸이 긴데 스택을 따라가기 어렵다. 어떻게 하는가?
답. pnx see 파일 이 정의의 줄마다 그 줄이 끝난 자리의 스택을 보여 준다(15장). 몸을 줄마다 끊어 적고, 중간 값에 to 이름 으로 이름을 붙이면(4장) 스택에 동시에 있는 값이 둘을 넘지 않는다.
정리
- 서명은 반드시 있고, 부르는 쪽과 몸 쪽에서 검사된다(표준 9.2).
- 먼저 올린 값이 첫 입력이다. 몸은 빈 스택에서 시작하고(이름 없는 입력은 스택에 남는다), 끝날 때 결과만 남긴다(9.2).
- 이름 있는 입력은 지역 변수가 되고, 타입만 쓴 입력은 스택에 남는다. 섞지 못한다(9.2).
- 결과가 여럿이면
to (a, b)로 받는다. 오른쪽이 맨 위다(9.4). - 보이는 이름(핵 낱말 포함)은 새 이름으로 쓰지 못한다(9.5).
- 꼬리 부름은 깊이가 자라지 않는다(9.6).