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 자료 파일

호스트는 소스를 자료 적재로 읽을 수 있다. 자료 적재는 선언과 값만 받고, 낱말 정의와 부르기, 제어를 모두 거절한다. 그래서 믿지 못하는 설정 파일을 읽어도 그 파일이 코드를 돌릴 수 없다.

10-data.c

#include "pinax.h"
#include <stdio.h>
#include <string.h>

/* A settings file the host reads as data: declarations and values, no code. */
static const char settings[] =
    "global port int\n"
    "global name str(16)\n"
    "8080 set port\n"
    "\"edge\" set name\n";
static const char hostile[] = "word boom () end\n";

int main(void) {
    static unsigned char block[1 << 20];
    pinax_config config = {.struct_size = sizeof config};
    pinax_state *st;
    if (pinax_open((pinax_mem){block, sizeof block}, &config, &st) != PINAX_OK)
        return 1;
    if (pinax_load(st, "settings", (pinax_str){(const unsigned char *)settings, strlen(settings)}, PINAX_LOAD_DATA) != PINAX_OK)
        return 1;
    pinax_global g;
    pinax_global_find(st, "port", &g);
    printf("port: %lld\n", (long long)*(const int64_t *)g.ptr);
    if (pinax_load(st, "hostile", (pinax_str){(const unsigned char *)hostile, strlen(hostile)}, PINAX_LOAD_DATA) != PINAX_OK) {
        pinax_str why = pinax_error_text(st);
        printf("refused: %.*s\n", (int)why.len, (const char *)why.ptr);
    }
    pinax_close(st);
    return 0;
}

출력

$ ./10-data
port: 8080
refused: a data source defines no words and uses no modules

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 비트 난수다(암호에 쓰는 것이 아니다).

토큰그 뒤의 스택일어난 일
11몇 번째 인자인지.
env.arg"alpha"그 인자. 없는 번호면 실패한다.
or_else (e) "?" end"alpha"성공이라 건너뛴다. 실패였다면 "?" 가 그 자리에 온다.
io.println—찍는다.

문답

물음. 모듈 파일 안에서 io.println 을 써도 되는가?

답. 모듈도 use io 를 적으면 된다. 다만 모듈을 적재하는 호스트가 io 를 등록하지 않았으면 그 모듈은 거절된다. 널리 쓰일 모듈은 호스트 모듈에 기대지 않고 결과를 돌려주는 편이 낫다.

정리