Primitives & Basic Types
Lean 4 has a carefully designed type system. Understanding its basic types—especially why Nat is the default for numbers—is essential.
Natural Numbers (Nat)
In most languages, integer literals like 42 become 32-bit or 64-bit integers. In Lean, they become Nat—natural numbers with arbitrary precision.
1#check 42 -- Nat2#check 0 -- Nat3#check 999999999 -- Nat (no overflow!)45#eval 2 ^ 100 -- Works perfectly6-- 1267650600228229401496703205376Why Nat? Because natural numbers are mathematically clean. They have no negative values, no overflow, and are perfect for reasoning and proofs. This makes Lean suitable for both programming and mathematics.
Deep Dive: Fixed-Width Integers
When you need fixed-width integers for performance or interoperability, Lean provides:
1#check (42 : UInt8) -- 8-bit unsigned2#check (42 : UInt32) -- 32-bit unsigned 3#check (42 : UInt64) -- 64-bit unsigned4#check (42 : Int32) -- 32-bit signed5#check (42 : Int64) -- 64-bit signed67-- These can overflow!8#eval (255 : UInt8) + 1 -- 0 (wraps around)Use fixed-width types only when necessary (e.g., interfacing with C code or specific binary formats).
Integers (Int)
When you need negative numbers, use Int. Unlike Nat, integers include negative values.
1#check (-5 : Int) -- Int2#check (42 : Int) -- Int (explicit cast)34#eval (-10 : Int) + 3 -- -75#eval (5 : Int) - 10 -- -567-- Warning: Nat subtraction saturates at 0!8#eval 5 - 10 -- 0 (because this is Nat)Nat never goes negative—it saturates at zero. If you need negative results, cast to Int first.Floating-Point Numbers (Float)
For decimal numbers, Lean uses 64-bit IEEE 754 floating-point:
1#check 3.14 -- Float2#check 2.5e10 -- Float (scientific notation)34#eval 3.14 * 2.0 -- 6.2800005#eval 1.0 / 3.0 -- 0.333333...Booleans (Bool)
Booleans in Lean are exactly what you expect:
1#check true -- Bool2#check false -- Bool34-- Boolean operations5#eval true && false -- false (and)6#eval true || false -- true (or)7#eval !true -- false (not)89-- Comparison returns Bool10#eval 5 > 3 -- true11#eval "a" == "b" -- falseCharacters and Strings
Char represents a single Unicode code point. String is a sequence of characters.
1#check 'a' -- Char2#check '∀' -- Char (Unicode works!)3#check "Hello" -- String45-- String operations6#eval "Hello" ++ " " ++ "World" -- "Hello World"7#eval String.length "Lean" -- 48#eval "Lean"[0]! -- 'L'910-- String interpolation11def name := "Lean"12#eval s!"Hello, {name}!" -- "Hello, Lean!"s!"..." syntax enables string interpolation. Curly braces{expr} insert the value of any expression.More String Operations
Strings have many useful methods for text processing:
1-- Splitting and joining2#eval "hello world".split (· == ' ') -- ["hello", "world"]3#eval " ".intercalate ["a", "b", "c"] -- "a b c"45-- Searching6#eval "hello".startsWith "he" -- true7#eval "hello".endsWith "lo" -- true8#eval "hello".containsSubstr "ell" -- true910-- Transforming11#eval "hello".toUpper -- "HELLO"12#eval "HELLO".toLower -- "hello"13#eval " hello ".trim -- "hello"14#eval "hello".replace "l" "L" -- "heLLo"1516-- Substrings17#eval "hello".take 3 -- "hel"18#eval "hello".drop 2 -- "llo"19#eval "hello".takeWhile (· != 'l') -- "he"Bounded Naturals (Fin n)
Fin n represents natural numbers less than n. This is crucial for safe array indexing where you can prove the index is in bounds.
1-- Fin n: numbers 0, 1, ..., n-12#check (⟨2, by omega⟩ : Fin 5) -- A number < 534-- Safe array indexing with Fin5def arr : Array String := #["a", "b", "c"]6def safeGet (arr : Array α) (i : Fin arr.size) : α := arr[i]78-- The type system ensures i is always valid9-- No runtime bounds checking needed!1011-- Creating Fin values12#eval (3 : Fin 10) -- 313#eval (0 : Fin 5) -- 01415-- Fin arithmetic wraps around16#eval (4 : Fin 5) + 1 -- 0 (wraps)Fin n is how Lean achieves safe array access. When you use arr[i] with a Fin arr.size, the compiler knows the access is always valid.Numeric Literal Type Inference
Numeric literals like 42 are polymorphic—their type depends on context:
1-- Type inferred from usage2def natVal := 42 -- Inferred as Nat (default)3def intVal : Int := 42 -- Int, because we said so4def floatVal : Float := 42-- Float, coerced from literal56-- Context determines type7def needsInt (x : Int) : Int := x * 28#eval needsInt 42 -- 42 is treated as Int here910-- Explicit type ascription11#eval (42 : UInt8) -- 42 as 8-bit unsigned12#eval (42 : Int32) -- 42 as 32-bit signed1314-- Default is always Nat15#check 42 -- NatChoosing the Right Numeric Type
A good rule of thumb: use Nat for counts and sizes, Intfor values that can go negative, and fixed-width integers only when you need binary compatibility or performance constraints.
Nat: sizes, lengths, indices, and proof-friendly arithmetic.Int: balances, offsets, or any signed values.UInt8/UInt32: file formats, bit-level operations, C interop.
The Unit Type
Unit is a type with exactly one value: (). It's used when a function doesn't need to return meaningful data.
1#check () -- Unit2#check Unit -- Type34-- A function that "returns nothing"5def printHello : IO Unit := IO.println "Hello"Type Annotations
You can explicitly specify types using the colon syntax:
1-- Explicit type annotations2def x : Nat := 423def y : Int := -54def z : Float := 3.1456-- In expressions7#eval (42 : Int) - 100 -- -5889-- Type ascription in complex expressions10def result := ((5 : Int) - 10) * 2 -- -10Type Summary
| Type | Description | Examples |
|---|---|---|
| Nat | Arbitrary-precision natural numbers | 0, 42, 2^100 |
| Int | Arbitrary-precision integers | -5, 0, 42 |
| Float | 64-bit floating point | 3.14, 2.5e10 |
| Bool | Boolean values | true, false |
| Char | Unicode character | 'a', '∀' |
| String | Text string | "Hello" |
| Unit | Single value type | () |
Create a Nat and an Int with the same literal. Then subtract 10 from both and compare the results.
1def n : Nat := 52def i : Int := 534#eval n - 10 -- What happens?5#eval i - 10 -- What happens?