Syntax & Notation
Learn how Lean notation maps to function calls, how to read infix operators, and how to use Unicode symbols without confusion. Lean's syntax is designed for readability—once you understand the patterns, code becomes self-documenting.
Infix Operators are Functions
Most operators in Lean are just functions written in infix form. The +in 2 + 3 is actually calling a function. You can always use prefix notation if it helps understanding:
1-- Infix form (what you normally write)2#eval 2 + 3 -- 534-- Prefix form (same thing!)5#eval HAdd.hAdd 2 3 -- 567-- This works for all operators8#eval 10 - 3 -- 79#eval HSub.hSub 10 3 -- 71011-- Comparison operators too12#eval 5 > 3 -- true13#eval GT.gt 5 3 -- true1415-- You can use parentheses to make precedence explicit16#check (2 + 3) * 4 -- Nat#check. This reveals the underlying function.Unicode and ASCII Equivalents
Lean supports Unicode symbols for clarity, making code look more like mathematical notation. Most have ASCII alternatives, so you can choose whichever feels more comfortable:
1-- Function arrows2#check Nat → Nat -- Unicode arrow (\to)3#check Nat -> Nat -- ASCII arrow45-- Universal quantifier6#check ∀ n : Nat, n = n -- Unicode (\forall)7#check forall n : Nat, n = n89-- Existential quantifier10#check ∃ n : Nat, n > 0 -- Unicode (\exists)11#check Exists fun n => n > 01213-- Product types14#check α × β -- Unicode (\times)15#check Prod α β -- Prefix form1617-- Lambda18#check λ x => x + 1 -- Unicode (\l)19#check fun x => x + 1 -- ASCII2021-- Common symbols22-- ∧ = and (\and) ∨ = or (\or)23-- ¬ = not (\neg) ≠ = ne (\ne)24-- ≤ = le (\le) ≥ = ge (\ge)\\forall, \\to, or \\alpha to insert Unicode symbols quickly. You can also hover over a Unicode symbol to see its input sequence.Dot Notation
Dot notation is shorthand for passing the left side as the first argument. It makes code read more naturally, especially for chained operations:
1-- These are equivalent:2#check List.length [1, 2, 3]3#check [1, 2, 3].length -- Dot notation ✓45#check List.map (fun x => x * 2) [1, 2, 3]6#check [1, 2, 3].map (fun x => x * 2) -- Cleaner ✓78-- Chaining operations9#eval [1, 2, 3, 4, 5]10 .filter (· > 2) -- [3, 4, 5]11 .map (· * 10) -- [30, 40, 50]12 .reverse -- [50, 40, 30]1314-- String operations15#eval "Hello World".toUpper.drop 6 -- "WORLD"1617-- Works with nested types too18#eval (some [1, 2, 3]).map List.length -- some 3The · (Dot) Placeholder
The centered dot · creates a short lambda. It stands for "the argument goes here."
1-- These are equivalent:2#eval [1, 2, 3].map (· + 1) -- [2, 3, 4]3#eval [1, 2, 3].map (fun x => x + 1) -- [2, 3, 4]45-- More examples6#eval [1, 2, 3].filter (· > 1) -- [2, 3]7#eval [1, 2, 3].map (· * ·) -- Error! Only one · allowed89-- Use for comparison10#eval [2, 1, 3].mergeSort (· < ·) -- Error! Can't use two dots11#eval [2, 1, 3].mergeSort (fun a b => a < b) -- [1, 2, 3]· placeholder works best for simple expressions. For complex lambdas with multiple arguments, use fun.Common Mistakes
Mistake 1: Using = instead of :=
1-- ❌ Wrong: = is for equations/proofs2-- def x = 5 34-- ✓ Correct: := is for definitions5def x := 567-- = is used in propositions8example : 2 + 2 = 4 := rflMistake 2: Confusing -> and =>
1-- -> (or →) is for function TYPES2#check Nat -> Nat -- A function type3#check Nat → Nat -- Same thing45-- => is for lambda BODIES and match arms6#check fun x => x + 1 -- A lambda expression7example : Nat -> Nat := fun x => x * 289-- In match expressions10def describe (n : Nat) : String :=11 match n with12 | 0 => "zero" -- => in match arms13 | _ => "positive"Mistake 3: Precedence surprises
1-- Function application has high precedence2#check List.map (· + 1) [1, 2, 3] -- ✓3-- #check List.map · + 1 [1, 2, 3] -- ❌ Confusing45-- Be explicit when chaining6#eval (1 + 2) * 3 -- 97#eval 1 + 2 * 3 -- 7 (multiplication first)89-- Logical operators10#check true && false || true -- (true && false) || true11#check true && (false || true) -- Explicit is clearerPrecedence & Parentheses
Lean follows standard mathematical precedence, but when in doubt, add parentheses. This is especially useful when learning new operators.
1#check 2 + 3 * 4 -- parsed as 2 + (3 * 4)2#check (2 + 3) * 4 -- explicit grouping34#check 1 + 2 + 3 -- left associative: (1 + 2) + 35#check 2 ^ 3 ^ 2 -- right associative: 2 ^ (3 ^ 2) = 51267-- Function application is left-associative8#check f x y z -- ((f x) y) z910-- Type arrows are right-associative11#check Nat → Nat → Nat -- Nat → (Nat → Nat)Named Arguments
Named arguments make code more readable and let you pass arguments in any order:
1def greet (name : String) (formal : Bool := false) : String :=2 if formal then s!"Good day, {name}." else s!"Hey {name}!"34-- Positional5#eval greet "Alice" true67-- Named (order doesn't matter!)8#eval greet (formal := true) (name := "Bob")910-- Especially useful with many similar arguments11def createUser (name : String) (email : String) (age : Nat) := 12 s!"{name} ({email}), age {age}"1314#eval createUser 15 (name := "Alice") 16 (email := "[email protected]") 17 (age := 30)Pipeline Operators
Pipeline operators help with readable data transformations:
1-- Forward pipe: passes result to next function2#eval [1, 2, 3] |> List.reverse |> List.length -- 334-- Equivalent to:5#eval List.length (List.reverse [1, 2, 3]) -- 367-- Chained example8def process (xs : List Nat) : Nat :=9 xs 10 |> List.filter (· > 0)11 |> List.map (· * 2)12 |> List.sum1314#eval process [0, 1, 2, 3] -- 12Rewrite the following expression using prefix notation and verify that Lean accepts it:
1-- Infix form2#eval (2 + 3) * 4 -- 2034-- Prefix form5#eval HMul.hMul (HAdd.hAdd 2 3) 4 -- 20Rewrite using dot notation and chain the operations:
1-- Before2#eval List.length (List.reverse [1, 2, 3])34-- After (with dot notation)5#eval [1, 2, 3].reverse.length -- 3Use · to simplify these lambdas:
1-- Before2#eval [1, 2, 3].map (fun x => x * 2)3#eval [1, 2, 3].filter (fun x => x > 1)45-- After6#eval [1, 2, 3].map (· * 2) -- [2, 4, 6]7#eval [1, 2, 3].filter (· > 1) -- [2, 3]