Lesson 7.2
Monk's Module Syntax
The four import forms, the export statement, and how names cross file boundaries.
Exporting names.
By default, everything in a .monk file is private. To make a name visible to other files, use export.
// math.monk
let add = (a int, b int) int { return a + b }
export add
let mul = (a int, b int) int { return a * b }
export mul
const PI = 3.14159
export PI You can export functions, variables, constants, and type declarations. You cannot export a name that isn't declared in the same file.
Trying to import a name that wasn't exported is a compile error — the module resolver validates exports before type-checking begins.
Module-level code (top-level statements outside functions) also runs, even if nothing is exported. A module that only has side effects (e.g. initialization, logging) can be valid.
The four import forms.
Monk has four ways to import from another module. All four resolve to the same thing at the C level — they only differ in how names are brought into scope.
Single import
use add from "./math" Imports one name. add is now in scope, bound to the function from math.monk.
Destructured import
use { add, mul } from "./math" Imports multiple names in one statement. Each name is validated against the module's exports individually.
Wildcard import
use * from "./math" Imports everything the module exports. Skips individual name validation — the type checker handles resolution. Useful for modules with many exports.
Alias import
use add as plus from "./math" Imports one name under a different local name. plus is in scope; add is not. Useful for resolving name collisions or shortening long names.
All four forms are validated at compile time. You cannot import a name that doesn't exist in the module's exports, and you cannot use a module value that wasn't imported. There are no implicit globals from imported modules.
Module paths are always relative.
The source string in a use statement must start with . — it is always a relative path from the importing file. No bare module names, no absolute paths.
use add from "./math" // math.monk in the same directory
use helpers from "./lib/utils" // utils.monk in a subdirectory
use base from "../shared" // shared.monk in the parent directory
The .monk extension is optional — the resolver adds it automatically if missing.
use add from "./math" // same as:
use add from "./math.monk" // identical
If the file doesn't exist at the resolved path, the compiler reports a specific error at the line where the use statement appears.
A complete example.
The examples/modules/ directory has the canonical working example:
math.monk
let add = (a int, b int) int { return a + b }
export add
let mul = (a int, b int) int { return a * b }
export mul
const PI = 3
export PI main.monk
use { add, mul } from "./math"
use PI from "./math"
show("add(3, 4) = " + to_string(add(3, 4)))
show("mul(5, 6) = " + to_string(mul(5, 6)))
show("PI = " + to_string(PI))
let result = add(mul(3, 4), mul(5, 6))
show("3*4 + 5*6 = " + to_string(result))
Compile and run: monk run examples/modules/main.monk. The compiler detects the use statements, resolves math.monk, checks types across the boundary, and emits a single binary.
What the type checker sees across the boundary.
When main.monk calls add(3, 4), the type checker knows:
add was imported from math.monk, which exported it.
math.monk was type-checked first (dependency order), so add's signature is already in the type map: (int, int) -> int.
Passing 3, 4 (both int) matches the signature. The call is valid.
Passing "hello", 4 would be a compile error: cannot pass string to int parameter.
The type boundary is fully enforced. Modules do not relax the type checker.