10 모듈과 자료 파일
먼저 알아야 할 것: 1장(use), 3장(서명).
이 장이 답하는 물음
- 프로그램을 여러 파일로 나누려면?
- 모듈은 무엇을 밖에 보이고 무엇을 숨기는가?
- 설정 파일을 코드 없이 읽으려면?
10.1 모듈 만들기
파일 하나가 모듈 하나다. 모듈의 이름은 파일 이름에서 .pnx 를 뗀 것이다. 밖에 보일 것에는 export 를 붙인다.
money.pnx
rem A small module: amounts are cents, kept apart from plain numbers.
export type cents int
word pad2 (n int gives str) n 10 lt if cat ("0", n) else cat (n) end end
export word of_won (won int gives cents) won 100 mul cents end
export word show (c cents gives str) cat (c raw 100 div, ".", c raw 100 mod pad2) end
cents 는 int 위의 선언한 타입이다. 표현은 정수이지만 int 와 섞이지 않는다. raw 가 바탕의 정수를 꺼낸다. cents 를 정수로 곱하거나 나누면 결과도 cents 다(c 3 mul). 몇 배 해도 단위는 그대로이기 때문이다. 정수가 필요한 show 의 나눗셈만 raw 를 쓴다.
10.2 모듈 쓰기
use money 를 쓰면 money.of_won 처럼 경로로 쓴다. 내보낸 타입 cents 는 이름만으로 쓴다. pnx 는 money 가 등록된 모듈이 아니면 프로그램 옆의 money.pnx 를 먼저 적재한다.
10-use.pnx
use io
use money
word total (a cents, b cents gives cents) a b add end
cat ("total ", 12 money.of_won 3 money.of_won total money.show) io.println
출력
$ pnx 10-use.pnx
total 15.00
10-use-as.pnx
use io
use money as m
cat ("price ", 7 m.of_won m.show) io.println
출력
$ pnx 10-use-as.pnx
price 7.00
내보내지 않은 pad2 는 밖에서 보이지 않는다.
10-private.pnx
use money
5 money.pad2 drop
거절
$ pnx 10-private.pnx
10-private.pnx:2:3: the module exports nothing of this name
모듈은 낱말 말고도 상수, 전역, 타입, 레코드를 내보낼 수 있다. 쓰는 꼴은 이렇다.
| 내보낸 것 | 읽기·부르기 | 비고 |
|---|---|---|
| 낱말 | 모듈.낱말 | 서명은 모듈 안과 같다 |
상수 (const) | 모듈.이름 | |
전역 (global 과 모음) | 모듈.이름 | 넣을 때는 값 set 모듈.이름 |
| 타입, 레코드, 표의 줄 타입 | 이름만 | cents 처럼 경로 없이 쓴다 |
아래 모듈 tally 는 상수 하나, 전역 하나, 낱말 하나를 내보낸다. 전역을 바꾸는 것은 대개 모듈 자신의 낱말(bump)이 맡지만, 밖에서도 set tally.hits 로 넣을 수 있다.
tally.pnx
rem A module that keeps a count: an exported constant, an exported global and a word.
export const limit 3
export global hits int
export word bump (gives bool) rem true while hits is still under the limit
hits limit lt if hits 1 add set hits true else false end
end
10-tally.pnx
use io
use tally
word try_once ()
tally.bump if "counted" else "limit reached" end io.println
end
try_once try_once try_once try_once rem the fourth time the limit is reached
cat ("hits ", tally.hits, " of ", tally.limit) io.println rem read an exported global and constant
0 set tally.hits rem an exported global is written with set module.name
cat ("hits after reset ", tally.hits) io.println
출력
$ pnx 10-tally.pnx
counted
counted
counted
limit reached
hits 3 of 3
hits after reset 0
use 한 모듈을 찾지 못하면 적재 때 거절된다. pnx 는 표준 모듈과 호스트 모듈을 먼저 보고, 없으면 프로그램 파일 옆의 이름.pnx 를 찾는다.
10-unknown-module.pnx
use maths rem no maths.pnx beside the program, no such module
거절
$ pnx 10-unknown-module.pnx
10-unknown-module.pnx:1:5: no module of this name is registered or loaded
문답
물음. 모듈의 이름은 누가 정하는가?
답. 적재하는 쪽이다. pnx 는 파일 이름에서 .pnx 를 뗀 것을 쓰고, C 호스트는 pinax_load 에 이름을 준다. 그래서 파일 안에는 모듈의 이름을 적는 줄이 없다. 이름이 같은 모듈 둘은 한 상태에 함께 있지 못한다.
흔한 오해: cents 도 결국 정수니까 더해도 된다
cents 와 int 를 더하면 거절된다. 같은 선언한 타입끼리만 더하고 뺀다. 단위가 다른 수를 섞는 실수를 막으려고 타입을 나눈 것이다.10-mixed.pnx
use money
12 money.of_won 3 add drop
거절
$ pnx 10-mixed.pnx
10-mixed.pnx:2:19: a declared type is added to and subtracted from its own type, and multiplied, divided or taken modulo by an int; for other arithmetic take raw
10.3 모듈의 맨 이름
같은 모듈을 줄곧 부르는 스크립트, 무엇보다 셸 스크립트는 찾는 순서에 그 모듈들을 이름 붙일 수 있다. 그러면 그 모듈의 낱말을 맨 이름으로 쓴다.
10-search.pnx
search (io, str)
"pinax" upper println
"a,b,c" "," "+" replace println
출력
$ pnx 10-search.pnx
PINAX
a+b+c
순서는 마지막에 쓰인다. 맨 이름은 먼저 지역 변수, 다음은 소스의 이름, 다음은 핵 낱말이다. 그다음에야 순서의 모듈을 왼쪽부터 찾는다. 그래서 str 이 순서에 있어도 len 은 핵 낱말이고, str 의 낱말은 str.len 으로 쓴다. search () 는 순서를 다시 비운다. 경로는 순서가 있든 없든 쓸 수 있다.
10.4 자료 파일
호스트는 소스를 자료 적재로 읽을 수 있다. 자료 적재는 선언과 값만 받고, 낱말 정의와 부르기, 제어를 모두 거절한다. 그래서 믿지 못하는 설정 파일을 읽어도 그 파일이 코드를 돌릴 수 없다.
pnx 는 자료 적재를 하지 않는다. 자료 적재는 호스트의 일이다(14장).
10.5 호스트가 빌려주는 모듈
io file clock random env 는 표준 모듈이 아니라 호스트 모듈이다: 언어에 들어 있지 않고, 호스트가 등록할 때만 있다(14장). pnx 는 다섯을 모두 등록한다. 그래서 파일과 시계와 환경에 닿는 길은 호스트가 정한다 — 믿지 못하는 스크립트에는 file 을 주지 않으면 된다.
10-host-modules.pnx
rem run: args alpha beta
use io
use str
use env
use random
use clock
word demo ()
cat ("arguments: ", env.arg_count) io.println rem 0 is the program file
1 env.arg or_else (e) "?" end io.println
"PINAX_NO_SUCH_VARIABLE" env.getenv or_else (e) cat ("getenv failed: ", e err_text) end io.println
7 random.seed rem the same seed, the same numbers
cat (10 random.random_below, " ", 10 random.random_below, " ", 10 random.random_below) io.println
clock.monotonic_ns to a
clock.monotonic_ns to b
cat ("the clock does not go back: ", b a lt not) io.println
end
demo
출력
$ pnx 10-host-modules.pnx
arguments: 1
?
getenv failed: not_found
7 2 8
the clock does not go back: true
env.arg_count 와 env.arg 는 명령줄의 인자이고(0 번은 프로그램 파일), env.getenv 는 환경 변수다. random.seed 뒤의 random.random_below 는 같은 씨앗에 같은 수열을 내므로 시험에서 쓰기 좋다. clock.monotonic_ns 는 거꾸로 가지 않는 시계라 걸린 시간을 재는 데 쓰고, clock.now_ms 는 달력의 시각이며, clock.sleep 은 밀리초만큼 멈춘다. random.random 은 범위 없는 64 비트 난수다(암호에 쓰는 것이 아니다).
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
1 | 1 | 몇 번째 인자인지. |
env.arg | "alpha" | 그 인자. 없는 번호면 실패한다. |
or_else (e) "?" end | "alpha" | 성공이라 건너뛴다. 실패였다면 "?" 가 그 자리에 온다. |
io.println | — | 찍는다. |
문답
물음. 모듈 파일 안에서 io.println 을 써도 되는가?
답. 모듈도 use io 를 적으면 된다. 다만 모듈을 적재하는 호스트가 io 를 등록하지 않았으면 그 모듈은 거절된다. 널리 쓰일 모듈은 호스트 모듈에 기대지 않고 결과를 돌려주는 편이 낫다.
정리
- 파일 하나가 모듈이고,
export한 것만 밖에서 보인다(표준 13.1, 13.2). use 모듈 [as 별명]뒤에모듈.이름으로 쓴다(13.1).- 낱말, 상수, 전역은 경로로, 타입과 레코드는 이름만으로 쓴다. 내보낸 전역에는
set 모듈.이름으로 넣는다(13.2). - 선언한 타입은 같은 타입끼리만 더하고 뺀다(7.4).
- 자료 적재는 선언과 값만 받는다(13.4).