Lean organizes code as a hierarchy of modules. A module corresponds to a file, and a namespace provides a logical grouping of declarations inside that file. Correct use of files, namespaces, and imports determines whether a project remains manageable as it grows.
Problem
You want a consistent way to organize code so that names are predictable, imports resolve correctly, and large developments remain navigable.
Solution
Use a one-to-one mapping between file paths and module names, and use namespaces to structure declarations within modules.
A file:
Myproj/Algebra/Group.leandefines the module:
import Myproj.Algebra.GroupInside the file, define a namespace that reflects the logical structure:
namespace Myproj.Algebra
structure Group where
carrier : Type
mul : carrier -> carrier -> carrier
end Myproj.AlgebraModules
A module is the unit of compilation. Each .lean file is compiled independently, subject to its imports.
Properties:
- modules form a directed acyclic graph via imports
- a module only sees declarations from imported modules
- recompilation follows the import graph
Imports must appear at the top of the file:
import Myproj.Basic
import Myproj.Algebra.GroupThere is no implicit import. If a name is not found, the module that defines it has not been imported.
Namespaces
A namespace groups related declarations and prevents name collisions.
namespace Myproj
def x : Nat := 1
end MyprojThe fully qualified name is:
Myproj.xNamespaces can be nested:
namespace Myproj.Algebra
def id {G : Type} (x : G) := x
end Myproj.AlgebraThis produces:
Myproj.Algebra.idOpening Namespaces
To avoid repeated qualification, you can open a namespace:
open Myproj.AlgebraNow id can be used directly.
A more controlled form:
open Myproj.Algebra (id)This imports only selected names.
Local opening inside a section:
section
open Myproj.Algebra
-- local usage
endThis avoids polluting the global scope.
Sections and Variables
Sections allow local parameters:
section
variable (α : Type)
def identity (x : α) := x
endAfter the section, identity becomes:
identity : {α : Type} -> α -> αThe variable is generalized into an implicit argument.
This mechanism reduces boilerplate and keeps definitions abstract.
Name Resolution
Lean resolves names using:
- local context
- opened namespaces
- imported modules
Ambiguity is resolved by qualification:
Myproj.Algebra.idYou can inspect names with:
#check Myproj.Algebra.idIf resolution fails, the usual causes are:
- missing import
- namespace not opened
- incorrect module path
Aliases and Renaming
You can introduce shorter names:
namespace Alg := Myproj.AlgebraThen:
Alg.idThis is useful in large developments with deep hierarchies.
Module Design Patterns
A stable layout follows these rules:
- one concept per file when possible
- directory structure mirrors conceptual structure
- minimal imports per file
- avoid circular dependencies
- prefer small modules over large ones
Example layout:
Myproj/
├── Data/
│ ├── List.lean
│ └── Tree.lean
├── Algebra/
│ ├── Group.lean
│ └── Ring.leanEach file defines a module with the same path.
Minimal Working Example
File:
-- Myproj/Utils.lean
namespace Myproj
def inc (n : Nat) : Nat :=
n + 1
end MyprojUse:
import Myproj.Utils
#eval Myproj.inc 5Expected result:
6Common Pitfalls
If import X.Y fails, check that the file is X/Y.lean.
If a name is not found, verify the namespace and imports.
If two modules define the same name, avoid open or use qualified names.
If definitions unexpectedly gain implicit arguments, inspect sections and variables.
If imports create cycles, refactor into smaller modules.
Takeaway
Files define modules, and modules define the compilation structure. Namespaces organize declarations and control visibility. A consistent mapping between directories, modules, and namespaces eliminates ambiguity and supports scalable development.