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 하나를 놓는다” 는 뜻이다. 그래서 낱말의 몸을 읽지 않고도 부르는 쪽의 스택을 따라갈 수 있다.

토큰그 뒤의 스택일어난 일
365365값을 올린다.
c_to_f977서명대로 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 b4 8이름을 쓰면 그 값이 올라간다.
add12더한다.
2 div62 로 나눈다. 몸이 끝날 때 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이름 없는 입력은 스택에 그대로 있다.
dup7 7하나 더 만든다.
mul49곱한다. 결과 하나가 남는다.

한 서명에서 이름 있는 입력과 이름 없는 입력을 섞지는 못한다. 섞으면 어느 값이 스택에 남는지 헷갈리기 때문이다.

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장) 스택에 동시에 있는 값이 둘을 넘지 않는다.

정리