Logic Gates
This is the level where Lean stops being “a functional language” and starts being a proof assistant. The whole idea rests on one move: statements are types, and proofs are values of those types.
Learning Goals
- Explain the difference between
BoolandPropin one sentence. - Read
theorem name : Statement := proofas an ordinary definition. - Prove simple facts with
rfl,decide, andtrivial. - Build and take apart
∧,∨, and→. - Understand why implication is literally a function type.
Two Different Worlds
Lean draws a sharp line between something you compute and something you prove. Both look like “true or false” at a glance, but they live in different universes.
1#check true -- Bool.true : Bool2#check (2 + 2 = 4) -- 2 + 2 = 4 : Prop34#check Bool -- Bool : Type5#check Prop -- Prop : Type| Bool | Prop | |
|---|---|---|
| What it is | A value | A statement |
| Has exactly | Two elements: true, false | Infinitely many, one per statement |
| You get an answer by | Running it (#eval) | Proving it |
| Equality written | == | = |
1-- Bool: computed2def isEven (n : Nat) : Bool := n % 2 == 03#eval isEven 4 -- true4#eval isEven 7 -- false56-- Prop: proven7def IsEven (n : Nat) : Prop := n % 2 = 08#check IsEven 4 -- IsEven 4 : Prop910theorem four_is_even : IsEven 4 := rflBool-valued functions get lowerCamelCase names, Prop-valued ones get UpperCamelCase. When you see IsEven versus isEven in a real codebase, the capital letter is telling you which world you are in.The Central Idea: Proofs Are Values
Look at that last line again. theorem four_is_even : IsEven 4 := rfl has exactly the shape of an ordinary definition: name : Type := value. That is not a coincidence or a pun. In Lean:
- A proposition is a type.
- A proof is a value of that type.
- To prove a statement is to construct a value of the corresponding type.
So 2 + 2 = 4 is a type, and rfl is a value that inhabits it. A false statement like 2 + 2 = 5is also a perfectly good type — it just has no values, and no amount of cleverness will produce one. “Unprovable” and “empty type” are the same thing.
Your First Proofs
rfl — true by computation
rfl stands for reflexivity: it proves a = b whenever the two sides reduce to literally the same thing. Lean runs both sides and checks.
1example : 2 + 2 = 4 := rfl2example : "ab" ++ "c" = "abc" := rfl3example : [1, 2].length = 2 := rflexample is an anonymous theorem. Use it when you want to check something without cluttering the namespace with a name you will never reference.
decide — let Lean do the case check
Many propositions are decidable: there is an algorithm that settles them. decide runs that algorithm and, if the answer is yes, converts the result into a real proof.
1example : 2 + 2 = 4 := by decide2example : 7 % 2 = 1 := by decide3example : ¬(1 = 2) := by decide45-- You can also just ask for the Bool answer6#eval decide (2 + 2 = 4) -- true7#eval decide (2 + 2 = 5) -- falsedecide only works on finite, computable checks. It cannot prove ∀ n : Nat, n + 0 = nbecause that would require testing infinitely many cases. For statements about all naturals you need induction — Level 8.One wrinkle worth knowing now: decide cannot see through a def on its own. If you wrap a proposition in a definition, unfold it first.
1def IsBig (n : Nat) : Prop := n > 10023-- theorem thousand_is_big : IsBig 1000 := by decide -- fails!4-- "failed to synthesize Decidable (IsBig 1000)"56theorem thousand_is_big : IsBig 1000 := by7 unfold IsBig -- goal becomes 1000 > 1008 decideWhy You Cannot #eval a Proposition
#eval (2 + 2 = 4) is an error, and now you can say precisely why. A proposition is a type, and types are not runtime values you can print. What you can evaluate is decide (2 + 2 = 4), which is the Bool that the decision procedure produces.
The bridge runs the other way too. When you use a Bool where Lean expects a Prop, it silently inserts = true.
1#check (isEven 4 = true) -- isEven 4 = true : Prop2example : isEven 4 = true := by decideDeep Dive: Why have two notions of truth at all?
It looks like duplication, but each does something the other cannot.
Bool is computable by construction. Every Bool can be evaluated to true or false in finite time, which is exactly what you need for code that runs.
Prop is expressive. You can state “every even number greater than 2 is the sum of two primes” as a Prop. There is no algorithm that decides it, so it could never be a Bool-valued function — but it is a perfectly meaningful statement, and Lean lets you write it down and reason about it.
There is a second, subtler benefit. Lean treats all proofs of the same proposition as interchangeable (proof irrelevance), and erases them entirely when compiling. Your proofs impose zero runtime cost: they are checked once, then deleted.
The Logical Connectives
Each connective has a proposition-building form and a corresponding way to construct or consume its proofs.
1#check (1 = 1 ∧ 2 = 2) -- and2#check (1 = 1 ∨ 1 = 2) -- or3#check ¬(1 = 2) -- not4#check (1 = 1 → 2 = 2) -- implies5#check (1 = 1 ↔ 2 = 2) -- if and only ifType these with \and, \or, \not, \to, \iff.
And: a pair of proofs
To prove P ∧ Qyou supply a proof of each side — and the syntax is the same anonymous constructor you used for structures in Level 4, because And is a structure.
1theorem both : 1 = 1 ∧ 2 = 2 := ⟨rfl, rfl⟩23-- Taking one apart: .left / .right, or .1 / .24theorem left_of (h : 1 = 1 ∧ 2 = 2) : 1 = 1 := h.left5theorem right_of (h : 1 = 1 ∧ 2 = 2) : 2 = 2 := h.267-- Swapping the two halves8example (p q : Prop) (h : p ∧ q) : q ∧ p := ⟨h.2, h.1⟩Or: a choice of side
To prove P ∨ Q you must say which side you can prove. Or.inl takes the left, Or.inr the right.
1theorem eitherL : 1 = 1 ∨ 1 = 2 := Or.inl rflImplication is a function
Here the propositions-as-types idea earns its keep. A proof of P → Q is a function that converts any proof of P into a proof of Q. Not “analogous to” a function — the same arrow, the same lambda, the same rules you learned in Level 3.
1theorem impl : 1 = 1 → 2 = 2 := fun _ => rfl23-- In tactic mode, intro is how you introduce the argument4example (p q : Prop) (hp : p) (hq : q) : p ∧ q := by5 exact ⟨hp, hq⟩And since implication is a function type, ∀is a function type too — one whose return type is allowed to mention the argument.
1theorem all_add_zero : ∀ n : Nat, n + 0 = n := fun _ => rfl23theorem exists_big : ∃ n : Nat, n > 3 := ⟨4, by decide⟩4-- To prove an ∃ you supply a witness and a proof about it.Tactic Mode
Everything above was written as a direct term. The by keyword drops you into tactic mode, where you build the proof by giving instructions instead. Both produce the same underlying value.
1example : 1 = 1 ∧ 2 = 2 := by2 constructor -- split the ∧ into two goals3 · rfl -- prove the first4 · rfl -- prove the second56example (a b : Nat) (h : a = b) : b = a := by7 rw [h] -- rewrite using the hypothesis·bullets focus on one goal at a time. Keep the Infoview open while writing tactics — it shows exactly which goals remain after each step, and that feedback loop is the whole reason tactic mode is pleasant.Practice
Define a predicate IsPositive on integers, then prove that 3 satisfies it.
Show solution
1def IsPositive (n : Int) : Prop := n > 023#check IsPositive 3 -- IsPositive 3 : Prop45theorem three_pos : IsPositive 3 := by6 unfold IsPositive7 decideThe unfold step is the interesting part. Without it, decide sees an opaque IsPositive 3 and has no decision procedure for it.
Write a Bool-valued isVowel that tests whether a character is one of the five vowels. Then evaluate it on two inputs. Ask yourself which world this belongs in and why.
Show solution
1def isVowel (c : Char) : Bool :=2 c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'34#eval isVowel 'e' -- true5#eval isVowel 'z' -- falseBool is right here: the test is a finite computation you want to actually run inside a program. Reach for Prop when you want to state a fact and reason about it, not execute it.
Prove 2 < 3 ∧ 3 < 4. Then prove the general swap: for any propositions p and q, a proof of p ∧ q gives a proof of q ∧ p.
Show solution
1theorem small_and : 2 < 3 ∧ 3 < 4 := ⟨by decide, by decide⟩23theorem and_swap (p q : Prop) (h : p ∧ q) : q ∧ p := ⟨h.2, h.1⟩The second one works for every pair of propositions, and you never needed to know what p and q say. That is what makes it a logical law rather than a fact about numbers.
Common Mistakes
- Trying to
#evala proposition. Wrap it indecide. - Using
decideon a statement about all naturals. Infinite case checks do not terminate. You want induction. - Confusing
=with==. The first builds aProp, the second computes aBool. - Expecting
decideto unfold your definitions. It will not.unfoldfirst.
Bool is computed; Prop is proven. The bridge between them is decide. Underneath it all sits one idea — propositions are types and proofs are values — which is why ∧ is a pair and → is a function.Check Yourself
- Is
2 + 2 = 5a validProp? What does it mean for it to be false? - What kind of value is a proof of
P → Q? - Why does
decidefail on∀ n : Nat, n + 0 = n? - How do you extract the right half of a proof of
P ∧ Q?