Module System
Praia’s module system uses grains (like sand grains). A grain can be a single .praia file or a directory with multiple files.
Creating a grain
Section titled “Creating a grain”The quickest way to start a grain project is with sand init:
mkdir mygrain && cd mygrainsand initThis creates a grain.yaml manifest and a main.praia entry file. For grains that include a native C++ plugin, use sand init --plugin instead (see Native Plugins).
You can also create a grain manually. A grain is any .praia file that ends with an export statement:
let PI = 3.14159
func square(x) { return x * x }func cube(x) { return x * x * x }
export { PI, square, cube }Importing a grain
Section titled “Importing a grain”Use use to import. The grain is bound to a variable named after the last path segment:
use "math"
print(math.PI) // 3.14159print(math.square(5)) // 25Custom alias
Section titled “Custom alias”Use as to bind to a different name:
use "logger" as loguse "collections" as col
let l = log.create("App")This is required for grain names with hyphens:
use "my-grain" as myGrainmyGrain.doSomething()Relative imports
Section titled “Relative imports”Paths starting with ./ or ../ are resolved relative to the importing file:
use "./helpers/greeter"greeter.hello("world")Multi-file grains
Section titled “Multi-file grains”A grain can be a directory with a grain.yaml manifest:
ext_grains/ mylib/ grain.yaml <- specifies entry point main.praia <- main file helpers.praia <- internal moduleThe grain.yaml specifies the entry file:
name: mylibversion: 0.1.0main: main.praiaFiles within a grain directory can import each other with relative paths:
use "./helpers"
func process(x) { return helpers.double(x) }export { process }Resolution order
Section titled “Resolution order”When you write use "math", Praia looks for the grain in this order:
ext_grains/— local dependencies (installed by sand), walks up from the current filegrains/— project-bundled grains, walks up from the current file~/.praia/ext_grains/— user-global grains (sand --global)<libdir>/ext_grains/— system-global grains (sudo sand --global)
At each location, Praia checks for:
<name>.praia(single-file grain)<name>/directory withgrain.yaml(readsmainfield for entry file)<name>/main.praia(fallback if nograin.yaml)
- No duplicate imports — importing the same grain twice in one file is an error
- Grains run once — if multiple files import the same grain, it is only executed the first time; subsequent imports get the cached exports
- Isolated scope — grains cannot access the importer’s variables; they only see globals and their own definitions
- Explicit exports — only names listed in
export { ... }are visible to the importer
use "math"use "math" // Error: Grain 'math' is already imported in this fileGrains importing other grains
Section titled “Grains importing other grains”Grains can import other grains:
use "math"
func circleArea(r) { return math.PI * math.square(r)}
export { circleArea }Project structure
Section titled “Project structure”A typical Praia project:
my-project/├── ext_grains/ <- installed by sand│ └── router/│ ├── grain.yaml│ ���── main.praia├── grains/ <- project-bundled grains│ ├── math.praia�� └── geometry.praia├── grain.yaml <- project manifest├── sand-lock.yaml <- lock file (auto-generated)└── main.praia