Best Practices
Writing idiomatic Lean 4 code requires more than just knowing syntax. Follow these guidelines to write code that is readable, maintainable, and aligned with the community.
Naming Conventions
Lean follows specific casing rules. Sticking to these makes your code look native.
- UpperCamelCase for types, structures, classes, namespaces, and modules.
Example:Nat,HashMap,BinaryTree,ToString - lowerCamelCase for functions, definitions, and local variables.
Example:myFunction,calculateSum,isPrime,List.map - snake_case for theorem and lemma names, describing the statement rather than naming it arbitrarily.
Example:Nat.add_comm,Nat.succ_le_succ,List.length_append
Naming Theorems: the Mathlib Convention
A theorem name describes its conclusion, reading left to right, using standard abbreviations for the symbols. Once you internalise the scheme you can often guess the name of a lemma you need — which is far faster than searching.
1-- The name spells out the statement:2theorem add_comm (a b : Nat) : a + b = b + a -- "add is comm(utative)"3theorem length_append : (as ++ bs).length = ... -- "length of append"4theorem succ_le_succ : n ≤ m → n.succ ≤ m.succ -- hypothesis → conclusion56-- Common vocabulary:7-- _eq_ for = _le_ for ≤ _lt_ for <8-- _ne_ for ≠ _mem_ for ∈ _sub_ for ⊆9-- _add_ for + _mul_ for * _neg_ for -x10-- _iff_ for ↔ _not_ for ¬ _of_ for "follows from"11--12-- So: le_of_lt : a < b → a ≤ b ("≤ follows from <")13-- not_lt_of_ge : a ≥ b → ¬(a < b)14-- mul_le_mul_of_nonneg_left ... (long names are GOOD here) ' marks a variant of an existing lemma (add_comm'); ? marks a function returning Option (head?); ! marks one that panics (head!); and D marks one taking a default (getD). These apply to definitions, not theorems.Explicit vs Implicit Types
Lean's type inference is powerful, but readability is king.
Do Type:
- Top-level function signatures (always!)
- Structure fields
- Ambiguous literals (e.g.,
(5 : Float))
1-- Good: explicit types for top-level2def add (x : Nat) (y : Nat) : Nat := x + y34-- Bad: relying on inference for public API5def add x y := x + y -- this does not even compile: Lean cannot infer x's typeDon't Type:
- Local variables where type is obvious
- Match arms where patterns imply types
Do Notation Usage
Use do notation for monadic code (IO, Option, etc.). Avoid raw bind (>>=) operators unless the pipeline is very short (1-2 steps).
1-- Good: readable, imperative style2def safeCompute (x : Nat) : Option Nat := do3 let y ← safeSubtract x 104 let z ← safeDivide y 25 return z67-- Avoid: confusing operator chains for complex logic8def hardToRead (x : Nat) : Option Nat :=9 safeSubtract x 10 >>= fun y => safeDivide y 2 >>= fun z => pure zTermination
Avoid partial unless absolutely necessary.partial functions cannot be used in proofs and can hide infinite loops. Try to use structural recursion or termination_by first.
Structure vs Class
| Use Structure | Use Class |
|---|---|
| Grouping data (Product types) | Defining behavior (Interfaces) |
| A Point with x/y coordinates | Standard equality check (BEq) |
| Users create values explicitly | Compiler finds instances automatically |
Pattern Matching
Prefer the dot syntax match x with or the function match shorthand when possible.
1-- Good: shorthand for simple cases2def isZero : Nat → Bool3 | 0 => true4 | _ => false56-- Good: name only the fields you need, with .. for the rest7structure Point where8 x : Int9 y : Int1011def getX (p : Point) : Int :=12 match p with13 | { x, .. } => x1415-- Better still: just use the accessor16def getX' (p : Point) : Int := p.xUse the Standard Library
Before writing a helper function, check List, Array, and String namespaces. Functions like filterMap,find?, any, and all are often built-in.
[]. to see List functions.