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 : Nat => n > 0) -- Prefix form: ∃ is notation for Exists1213-- Product types (α and β must be in scope — declare them first)14variable (α β : Type)15#check α × β -- Unicode (\times)16#check Prod α β -- Prefix form1718-- Lambda19#check λ x => x + 1 -- Unicode (\l)20#check fun x => x + 1 -- ASCII2122-- Common symbols23-- ∧ = and (\and) ∨ = or (\or)24-- ¬ = not (\neg) ≠ = ne (\ne)25-- ≤ = le (\le) ≥ = ge (\ge)\forall, \to, or \alpha followed by space or tab to insert Unicode symbols. Hover over any Unicode symbol to see its input sequence. If you need a literal backslash-word (say, in a string), press Esc to cancel the abbreviation.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 · (typed \.) creates a short lambda. It stands for "an argument goes here." You can use several dots in one expression: each occurrence becomes a new parameter, filled left to right.
1-- One dot = one parameter2#eval [1, 2, 3].map (· + 1) -- [2, 3, 4]3#eval [1, 2, 3].map (fun x => x + 1) -- same thing45-- Two dots = two parameters, in order of appearance6#eval [1, 2, 3].foldl (· + ·) 0 -- 6 (fun a b => a + b)7#eval [2, 1, 3].mergeSort (· ≤ ·) -- [1, 2, 3]89-- The dots are filled left to right, so order matters10#eval ([10, 3].foldl (· - ·) 0 : Int) -- -13, i.e. (0 - 10) - 3The real limitations are different from "only one dot":
1-- 1. You cannot REUSE an argument. Each · is a fresh parameter,2-- so (· * ·) is (fun a b => a * b), NOT "square".3#eval [1, 2, 3].map (fun x => x * x) -- [1, 4, 9] ✓ must use fun45-- 2. The lambda extends to the nearest enclosing parentheses.6-- That is what decides how much of the expression is captured.7#eval [1, 2, 3].map ((· + 1) * 2) -- (fun x => x + 1) * 2 — type error8#eval [1, 2, 3].map (· + 1 |> (· * 2)) -- inner parens make the intent explicit9#eval [1, 2, 3].map (fun x => (x + 1) * 2) -- [4, 6, 8] ✓ clearest· placeholder works best for simple expressions. As soon as you need to use an argument twice, nest a computation, or name something, switch to fun — it costs six characters and removes all ambiguity about where the lambda begins.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-associative8def f (a b c : Nat) : Nat := a + b + c9#check f 1 2 3 -- parsed as ((f 1) 2) 31011-- Type arrows are right-associative12#check Nat → Nat → Nat -- Nat → (Nat → Nat)1314-- Which is *why* application is left-associative: f : Nat → (Nat → (Nat → Nat)),15-- so (f 1) : Nat → Nat → Nat, and ((f 1) 2) : Nat → Nat. The two conventions16-- fit together so partial application "just works".17#check f 1 -- Nat → Nat → Nat18#check f 1 2 -- Nat → NatTwo Ways to Avoid Parentheses
Deeply nested parentheses are the main readability killer in functional code. Lean gives you two operators that remove them, and they point in opposite directions:
1-- <| applies the function on the LEFT to everything on the right2#eval List.length <| List.reverse [1, 2, 3] -- 33#eval List.length (List.reverse [1, 2, 3]) -- same, with parens45-- |> feeds the value on the LEFT into the function on the right6#eval [1, 2, 3] |> List.reverse |> List.length -- 378-- They compose in a common pattern: pipe the data, apply the last function9#eval [1, 2, 3] |> List.map (· * 2) |> List.sum -- 121011-- Both are plain ASCII — no Unicode input needed.12-- There is also |>. for "pipe into a method": xs |>.reverse |>.length|> when you are describing a sequence of steps applied to data, and <|when you just want to drop one pair of closing parens at the end of a line.Anonymous Constructor Notation ⟨ ⟩
When Lean already knows which type you are building, you can write⟨...⟩ (typed \< and\>) instead of naming the constructor. This shows up constantly once you reach structures and proofs.
1-- Pairs2#check (⟨1, 2⟩ : Nat × Nat) -- (1, 2)3#eval (⟨1, "a"⟩ : Nat × String) -- (1, "a")45-- It nests, and the nesting can be flattened6#check (⟨1, 2, 3⟩ : Nat × Nat × Nat) -- same as ⟨1, ⟨2, 3⟩⟩78-- It only works when the expected type is known.9-- #check ⟨1, 2⟩ -- error: invalid constructor ⟨...⟩, expected type must be knownString Interpolation
1-- s! builds a String, splicing in any value with a ToString instance2#eval s!"1 + 1 = {1 + 1}" -- "1 + 1 = 2"34def name := "Ada"5#eval s!"Hello, {name}! Your name has {name.length} letters."67-- m! is the same idea for messages that may contain expressions (used in8-- macros and error messages); f! produces formatted output that can wrap.9-- Escape a literal opening brace with a backslash:10#eval s!"literal brace: \{ }" -- "literal brace: { }"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]