rfl — Reflexivity
rflproves that something equals itself. It's the most fundamental tactic in Lean 4 and often the first you'll learn. Despite its simplicity, understanding when and why it works is essential for mastering proof writing.
The Basic Idea
Every value equals itself. This is the reflexivity property of equality—one of the most basic axioms in mathematics. In Lean, rfl(short for "reflexivity") is how you prove that two expressions are equal when they compute to the same value:
1-- Anything equals itself2example : 5 = 5 := rfl3example : "hello" = "hello" := rfl4example : [1, 2, 3] = [1, 2, 3] := rfl56-- Works for any type7example : true = true := rfl8example : () = () := rfl -- Unit typeBut rfl is more powerful than it looks. It works whenever Lean can computeboth sides to the same value. This means you can prove equalities that don't look identical at first glance:
1-- These compute to the same value2example : 2 + 2 = 4 := rfl -- 2 + 2 computes to 43example : 10 - 3 = 7 := rfl -- 10 - 3 computes to 74example : 2 * 3 + 1 = 7 := rfl -- 2 * 3 + 1 → 6 + 1 → 756-- String operations7example : "hel" ++ "lo" = "hello" := rfl89-- List operations10example : [1, 2] ++ [3] = [1, 2, 3] := rfl11example : [1, 2, 3].head? = some 1 := rflrfl works when both sides of an equality compute(or "reduce") to the same term. Lean evaluates as much as it can before comparing. This is called definitional equality.When to Use rfl
Reach for rfl in these situations:
- Closing trivial goals: When both sides of an equality are identical or compute to the same thing
- After rewriting: When
rwtransforms the goal tox = x - Verifying function definitions: To confirm that a function produces the expected output for specific inputs
- Testing computed values: To validate that complex expressions evaluate correctly
1-- Definitional equality: both sides compute to the same thing2example : (fun x => x + 1) 5 = 6 := rfl -- Function application34example : [1, 2, 3].length = 3 := rfl -- Method call56example : (if true then 1 else 2) = 1 := rfl -- Conditional evaluation78def double (n : Nat) : Nat := n * 29example : double 5 = 10 := rfl -- User-defined function1011-- Pattern matching computes12def isZero : Nat → Bool13 | 0 => true14 | _ => false15example : isZero 0 = true := rfl16example : isZero 7 = false := rflWhen rfl Fails
rflcan't prove things that require reasoning beyond computation. If Lean can't directly evaluate both sides to the exact same term, rfl will fail:
1-- These require actual reasoning, not just computation23-- example (a b : Nat) : a + b = b + a := rfl4-- ❌ Fails: nothing can reduce until a and b are known56-- example (n : Nat) : 0 + n = n := rfl7-- ❌ Fails: Nat.add recurses on its SECOND argument, so 0 + n is stuck89-- example (n : Nat) : n * 1 = n := rfl10-- ❌ Fails: n * 1 reduces to 0 + n, which is stuck (see above)1112-- example (xs : List Nat) : xs.reverse.reverse = xs := rfl13-- ❌ Fails: can't compute without knowing xs1415-- But note that this one DOES work, which surprises people:16example (n : Nat) : n + 0 = n := rfl -- ✓When rfl fails, you need other tactics:
simp— for simplification using known lemmasrw— to rewrite using equalitiesring— for algebraic identitiesomega— for linear arithmetic
rfl only works for definitional equality—when things compute to the same value. For propositionalequality (things that are equal but don't directly compute), you need other tactics.Common Mistakes
Here are pitfalls beginners often encounter with rfl:
Mistake 1: Expecting rfl to prove algebraic identities
1-- ❌ Wrong: rfl cannot prove commutativity - nothing reduces2-- example (a b : Nat) : a + b = b + a := rfl34-- ✓ Correct: use the lemma, or a decision procedure5example (a b : Nat) : a + b = b + a := Nat.add_comm a b6example (a b : Nat) : a + b = b + a := by omegaMistake 2: Confusing similar-looking expressions
1-- These look the same but aren't definitionally equal!2-- example (xs : List Nat) : xs ++ [] = xs := rfl -- ❌ Fails34-- The definition of ++ recurses on the first list5-- so xs ++ [] doesn't simplify without knowing xs6example (xs : List Nat) : xs ++ [] = xs := by simp -- ✓ WorksMistake 3: Forgetting which side reduction happens on
This is the single most confusing thing about rfl, and it comes entirely from how Nat.add is defined:
1-- Nat.add recurses on its SECOND argument:2-- n + 0 = n3-- n + (m+1) = (n + m) + 145-- So a literal on the RIGHT can be consumed step by step:6example (n : Nat) : n + 0 = n := rfl -- ✓ one step7example (n : Nat) : n + 2 = n + 1 + 1 := rfl -- ✓ two steps89-- But a variable on the RIGHT blocks everything:10-- example (n : Nat) : 0 + n = n := rfl -- ❌ stuck immediately11example (n : Nat) : 0 + n = n := Nat.zero_add n -- ✓ needs the theorem1213-- Same story for multiplication (also recursive on the right):14example (n : Nat) : n * 0 = 0 := rfl -- ✓15-- example (n : Nat) : n * 1 = n := rfl -- ❌ reduces to 0 + n, stuck16-- example (n : Nat) : 1 * n = n := rfl -- ❌ stuck immediately rfl. If it is on the left, or is a variable, you need Nat.zero_add, simp, omega, or induction.rfl as a Tactic vs Term
rfl can be used both as a proof term and as a tactic. Both are correct, but they behave slightly differently:
1-- As a term (no 'by') - direct proof term2example : 2 + 2 = 4 := rfl34-- As a tactic (with 'by') - enters tactic mode first5example : 2 + 2 = 4 := by rfl67-- The tactic version gives better error messages when it fails8-- It also allows you to add other tactics before it if needed9example : 2 + 2 = 4 := by10 -- could add 'show 4 = 4' or other tactics here11 rflRelated: Eq.refl and @rfl
rfl is actually notation for Eq.refl _, where Lean infers the value. Understanding this helps when you need more control:
1-- These are all equivalent2example : 5 = 5 := rfl3example : 5 = 5 := Eq.refl 54example : 5 = 5 := @rfl Nat 556-- Eq.refl has type: ∀ (a : α), a = a7#check @Eq.refl -- {α : Sort u} → (a : α) → a = a89-- Sometimes you need @rfl to help type inference10example : ([] : List Nat) = [] := @rfl (List Nat) []Deep Dive: Definitional vs Propositional Equality
Lean has two notions of equality, and understanding the difference is crucial:
- Definitional equality:Two terms compute to the same value. Checked automatically by Lean's kernel.
rflproves this. - Propositional equality: Two terms are provably equal via some chain of reasoning. Requires explicit proof with tactics or lemmas.
For example:
2 + 2and4are definitionally equaln + 0andnare definitionally equal —Nat.add_zeroexists as a named lemma, but its proof is literallyrfl0 + nandnare only propositionally equal —Nat.zero_addis proved by induction onna + bandb + aare only propositionally equal(proven byNat.add_comm)
Definitional equality is "built-in" and checked automatically. Propositional equality requires you to prove it explicitly.
Real-World Examples
Validating data transformations
1-- Verify JSON-like value transformations2def Config := List (String × Nat)34def defaultConfig : Config := [("timeout", 30), ("retries", 3)]56example : defaultConfig.length = 2 := rfl7example : (defaultConfig.lookup "timeout") = some 30 := rflTesting parsing functions
1-- Quick unit tests using rfl2def parseDigit : Char → Option Nat3 | '0' => some 04 | '1' => some 15 | '2' => some 26 | _ => none78example : parseDigit '1' = some 1 := rfl9example : parseDigit 'a' = none := rflClosing goals after rewriting
1-- rw already tries rfl for you, so usually you write just:2example (a b : Nat) (h : a = b) : b = a := by3 rw [h] -- goal becomes b = b, and rw closes it45example (x y z : Nat) (h1 : x = y) (h2 : y = z) : x = z := by6 rw [h1, h2] -- goal becomes z = z, closed78-- Adding an explicit rfl after these gives "No goals to be solved".9-- You only need rfl when the rewrite leaves a goal that is definitionally10-- true but not SYNTACTICALLY identical:11example (n : Nat) (h : n = 3) : n + 0 = 3 := by12 rw [h] -- goal: 3 + 0 = 3, which rw's rfl also handlesWhen Computation Gets Expensive
rflruns inside Lean's kernel. For Nat arithmetic that is fine — the kernel has special GMP-backed support for it — but for anything the kernel must unfold step by step, it can become slow.
1-- Perfectly fast: the kernel special-cases Nat arithmetic2example : 123456 + 789012 = 912468 := by rfl3example : 2 ^ 64 = 18446744073709551616 := by rfl45-- decide: for decidable propositions that are not equations6example : (5 : Nat) < 10 ∧ (3 : Nat) ≠ 4 := by decide78-- native_decide: compiles the check to machine code and trusts the result9-- example : someHugeComputation = expected := by native_decidenative_decide is a genuine escape hatch, not just a faster decide. It adds Lean.ofReduceBoolto your trusted base — meaning you are now trusting the Lean compiler, your C compiler, and the hardware, rather than only the ~5000-line kernel. Check for it with #print axioms, and reach for it only when decide genuinely cannot finish.Summary
| Situation | Use rfl? |
|---|---|
Goal is x = x | ✅ Yes |
| Both sides compute to same value | ✅ Yes |
Variable with a literal on the right (n + 0 = n) | ✅ Yes — this one reduces |
Variable on the right (0 + n = n) | ❌ No, use simp or Nat.zero_add |
Algebraic identity (like a + b = b + a) | ❌ No, use ring or lemma |
Close the goal using computation and rfl.
1example : (3 * 4) + 2 = 14 := by2 rflVerify that this function works correctly for the given input.
1def greet (name : String) : String := "Hello, " ++ name ++ "!"23example : greet "World" = "Hello, World!" := by4 rflThis proof fails with rfl. Can you explain why and fix it with the right tactic?
1-- This does NOT work:2-- example (n : Nat) : 0 + n = n := rfl3-- Why? Nat.add recurses on its second argument, so with n unknown4-- there is nothing to reduce.56-- Fix it, three ways:7example (n : Nat) : 0 + n = n := by simp8example (n : Nat) : 0 + n = n := Nat.zero_add n9example (n : Nat) : 0 + n = n := by omega1011-- Meanwhile, its mirror image needs no tactic at all:12example (n : Nat) : n + 0 = n := rfl