10 Modules and data files
Builds on: Chapter 1 (use), chapter 3 (signatures).
Questions this chapter answers
- How is a program split into several files?
- What does a module show, and what does it hide?
- How are settings read without running code?
10.1 Making a module
One file is one module, named after the file without .pnx. What is to be seen outside is marked 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 is a declared type over int: represented as an integer, it does not mix with int. raw takes the underlying integer out. Multiplying or dividing cents by an integer gives cents (c 3 mul): scaling keeps the unit. Only the division in show, which needs plain integers, uses raw.
10.2 Using a module
After use money, its words are written as paths such as money.of_won; the exported type cents is written by its name alone. When money is not a registered module, pnx first loads money.pnx from beside the program.
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
Output
$ 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
Output
$ pnx 10-use-as.pnx
price 7.00
pad2, not exported, is not seen outside.
10-private.pnx
use money
5 money.pad2 drop
Refused
$ pnx 10-private.pnx
10-private.pnx:2:3: the module exports nothing of this name
Besides words, a module can export constants, globals, types and records. They are used like this.
| Exported | Read or call | Note |
|---|---|---|
| word | module.word | the signature is the same as inside the module |
constant (const) | module.name | |
global (global and collections) | module.name | stored with value set module.name |
| type, record, row type of a table | the name alone | written without a path, like cents |
The module tally below exports a constant, a global and a word. Changing the global is usually the job of the module’s own word (bump), but it can also be stored from outside with 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
Output
$ pnx 10-tally.pnx
counted
counted
counted
limit reached
hits 3 of 3
hits after reset 0
A module that use cannot find is refused at load. pnx looks at the standard and host modules first, then for NAME.pnx beside the program file.
10-unknown-module.pnx
use maths rem no maths.pnx beside the program, no such module
Refused
$ pnx 10-unknown-module.pnx
10-unknown-module.pnx:1:5: no module of this name is registered or loaded
Q&A
Q. Who decides a module’s name?
A. Whoever loads it. pnx uses the file name without .pnx, and a C host gives the name to pinax_load. So there is no line in the file that names its module. Two modules of the same name cannot be in one state together.
Common mistake: cents is just an integer, so it can be added to one
cents and int is refused. Only two values of the same declared type are added and subtracted; the types are kept apart to stop numbers of different units from being mixed.10-mixed.pnx
use money
12 money.of_won 3 add drop
Refused
$ 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 Bare names from modules
A script that calls the same modules all the time, a shell script above all, can name them in a search order. Then their words are written bare.
10-search.pnx
search (io, str)
"pinax" upper println
"a,b,c" "," "+" replace println
Output
$ pnx 10-search.pnx
PINAX
a+b+c
The order is only a last resort. A bare name is first a local, then a name of the source, then a core word; only then are the modules of the order asked, left to right. So len stays the core word even with str in the order, and the str word is written str.len. search () empties the order again. A path works with or without it.
10.4 Data files
A host can read a source by data load. A data load accepts declarations and values only, and refuses word definitions, calls and control. So reading an untrusted settings file cannot run any code.
pnx does not do data loads; they are the host’s business (chapter 14).
10.5 Modules the host lends
io file clock random env are not standard modules but host modules: they are not part of the language, and exist only when the host registers them (chapter 14). pnx registers all five. So the host decides the ways to files, the clock and the environment — a script that is not trusted is given no 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
Output
$ pnx 10-host-modules.pnx
arguments: 1
?
getenv failed: not_found
7 2 8
the clock does not go back: true
env.arg_count and env.arg are the arguments of the command line (0 is the program file); env.getenv reads an environment variable. After random.seed, random.random_below gives the same numbers for the same seed, which suits tests. clock.monotonic_ns never goes back, so it measures how long something took; clock.now_ms is the calendar time, and clock.sleep pauses for that many milliseconds. random.random is a 64-bit random number without a range (not for cryptography).
| Token | Stack after it | What happened |
|---|---|---|
1 | 1 | Which argument. |
env.arg | "alpha" | That argument; a number past the last fails. |
or_else (e) "?" end | "alpha" | Skipped on success. On failure "?" takes the place. |
io.println | — | Prints it. |
Q&A
Q. May a module file use io.println?
A. Yes, with use io in the module too. But when the host that loads the module has not registered io, the module is refused. A module meant for many hosts is better off returning results than depending on a host module.
Summary
- One file is one module, and only what is
exported is seen outside (standard 13.1, 13.2). - After
use MODULE [as ALIAS]its parts are writtenMODULE.NAME(13.1). - Words, constants and globals are used by path, types and records by name alone; an exported global is stored with
set module.name(13.2). - Values of a declared type are added and subtracted only with the same type (7.4).
- A data load accepts declarations and values only (13.4).