Modules & Packages

A Nim program is a graph of modules. Each file is a module, each module has its own private namespace, and nothing is visible to another module unless you mark it with *. This lesson covers the export rule, the four import forms, initialization order, and how nimble turns a folder of modules into a package.

Modules and Import

One file is one module. To build real programs you split responsibilities across files and let the compiler join them; the export marker is the only thing standing between your internals and the rest of the program.

Defining and Using a Module

Two files, one library and one program. The library exports three symbols and keeps one helper private.

# mathx.nim — a library module
const Pi* = 3.14159                 # '*' exports the constant

proc square*(x: float): float =     # '*' exports the procedure
  x * x

proc secret(): int =                # no '*': invisible to importers
  42

proc describe*(): string =
  "secret is " & $secret()          # private helpers stay usable inside the module
# main.nim — the program that consumes the module
import mathx                        # module name = file name, same folder

echo Pi                             # 3.14159
echo square(3.0)                    # 9.0
# echo secret()                     # compile error: 'secret' is not exported
echo describe()                     # secret is 42

The compiler reports an unexported symbol exactly like a misspelling: the name simply does not exist outside the module. That is the point — the module boundary is enforced at compile time, not by convention.

Import Forms

import std/strutils                      # everything the module exports
from std/strutils import parseInt        # only the names you list
import std/[algorithm, sequtils]         # the bracket form: several modules at once
import std/tables as tbl                 # keep a handle for qualified access

var data = @[3, 1, 2]
data.sort                                # from algorithm, imported unqualified
echo data.sorted                         # 'sorted' returns a NEW sequence
echo tbl.newTable[string, int]()         # qualified through the alias, collision-free

Prefer from ... import when a symbol name is short and generic; it documents at the top of the file where that name comes from. Prefer an alias or a qualified import when two modules export the same name.

Visibility Beyond Procedures

type
  Counter* = object                  # exported type …
    value: int                       # … with a PRIVATE field: no direct poking

proc newCounter*(): Counter = Counter(value: 0)
proc increment*(c: var Counter) = inc c.value     # controlled mutation
proc value*(c: Counter): int = c.value            # read-only accessor

var count = newCounter()
count.increment()
count.increment()
echo count.value                     # 2
# count.value = 99                   # compile error: the field is private

Exporting the type but not the field is the standard Nim encapsulation idiom: callers can store and pass a Counter, but only the module can change its representation. You may later change value to a different layout without breaking a single caller.

Module Initialization

Importing a module also runs its top-level code — once per program, before your main body continues. Understanding that order prevents the classic "why did this print before anything else?" confusion.

Top-Level Statements

# lib.nim
echo "initializing lib"             # runs the first time 'lib' is imported
let limit* = 10
# main.nim
import lib                          # prints: initializing lib
import lib                          # second import is a no-op: init runs once
echo limit                          # 10

Keep top-level code in a library minimal: constants, type definitions, and cheap setup. Anything expensive belongs in an explicit init procedure so the caller decides when to pay for it.

when isMainModule

The same file can be a library and a program. isMainModule is true only when the file is the one passed to the compiler, so the demo code never runs for importers.

# cli.nim — usable as a library and runnable as a program
import std/os

proc greet*(name: string): string = "hello " & name

when isMainModule:                  # true only for 'nim r cli.nim'
  let name = if paramCount() > 0: paramStr(1) else: "world"
  echo greet(name)

include versus import

# include pastes the file's text into THIS module: same scope, no new namespace,
# no export markers, no boundary. Use it to split one module across files.
include "shared_defs.nim"

# import creates a real module boundary: private by default, explicit exports.
import shared_helpers
echo shared_helpers.publicName()

Rule of thumb: include for generated or platform-specific fragments of one module, import for everything that deserves a boundary.

Packages and Nimble

A package is a folder with a .nimble manifest at its root. The manifest declares metadata, dependencies and build targets; the nimble tool reads it to build, test, install and publish.

The Nimble Manifest

# myapp.nimble — package manifest read by the nimble tool
version     = "0.1.0"
author      = "Elucian Moise"
description = "Small Nim CLI example"
license     = "MIT"

srcDir      = "src"                 # modules live under src/
bin         = @["myapp"]            # compile src/myapp.nim into an executable

requires "nim >= 2.0.0"             # dependencies, resolved from the nimble store
nimble build      # compile every 'bin' target
nimble run        # build, then run the first binary
nimble test       # run the modules under tests/
nimble install    # install this package (or a dependency) locally

Project Layout

myapp/
  myapp.nimble            # manifest: metadata, deps, targets
  src/
    myapp.nim             # entry point named after the package (convention)
    mylib.nim             # library modules
  tests/
    test_mylib.nim        # files starting with 'test' are picked up by nimble test
  docs/

Name the entry module after the package: for a package mylib, the importable root module is src/mylib.nim. Consumers then write import mylib and never see the folder layout.

Compiler Configuration

# nim.cfg — plain configuration; the same folder as the .nimble file
--path:"src"                 # additional module search path
--define:release             # compile with optimizations
--warningAsError:"UnusedImport"
# config.nims — the same settings as a script, for logic the plain file cannot express
switch("path", "src")
if hostOS == "windows":
  switch("define", "windowsBuild")

Reach for config.nims only when a setting must be computed; nim.cfg is easier to read and review.

Review Checklist

  1. Export the smallest surface that callers need; keep fields private and expose accessors.
  2. Prefer from x import y so the origin of every name is obvious.
  3. Guard demo code with when isMainModule instead of deleting it.
  4. Keep top-level code cheap; move real setup into an explicit init.
  5. One responsibility per module, named after the responsibility — not after the type count.