-- Lecture Notes for October 22, 2026

set_option autoImplicit false
set_option linter.unusedVariables false
set_option warn.classDefReducibility false
set_option pp.proofs true

-- ----------------------------------------------------
-- * A few more tactics for propositions

-- In the Lean core, there are no special tactics
-- for the ∃ quantifier. Instead, just use
-- `exact ⟨n, h⟩` for the ∃-introduction rule,
-- and `obtain ⟨n, h⟩ := ...` for ∃-elimination.

-- Introduction:
example : ∃n : Nat, n + n = 6 := by
  exact ⟨3, rfl⟩

-- Elimination:
example (α: Type) (P : α → Prop) (h : ∃ x, P x) : ∃ x, P x ∧ True := by
  obtain ⟨n, w⟩ := h
  refine ⟨n, ?_⟩
  constructor
  . assumption
  . constructor

-- There is also a `use` tactic, which supplies the
-- witness and leaves the proof of witnesshood as a
-- new goal (unless it can be solved by reflexivity,
-- in which case it is proved automatically).
--
-- However, `use` is a Mathlib tactic, and doesn't
-- exist in core Lean. We didn't get it to work
-- in class.
--
-- example : ∃n : Nat, n + n = 6 := by
--   use 3

-- Finally, the `contradiction` tactic is useful for
-- proving any goal from contradictory hypotheses.

example (p q : Prop) (hp : p) (hnp : ¬p) : q := by
  contradiction

-- ----------------------------------------------------
-- * Equality and rewriting, continued

-- Last time, we saw that lemmas can be used for
-- rewriting, e.g.:
example (a b : Nat) : a + b = b + a := by
  rw [Nat.add_comm]

-- Sometimes this works incorrectly. For example, consider
example (a b c: Nat) : a + (b + c) = a + (c + b) := by
  --> Goal: a + (b + c) = a + (c + b)
  rw [Nat.add_comm]
  --> Goal: b + c + a = a + (c + b)

  -- The tactic applied the lemma to change `a + (b + c)`
  -- to `(b + c) + a`, which wasn't what we wanted!
  sorry

-- We can fix this by specifying some arguments to the
-- lemma. `Nat.add_comm b c` has type `b + c = c + b`,
-- so we get the rewrite that we want.
example (a b c: Nat) : a + (b + c) = a + (c + b) := by
  --> Goal: a + (b + c) = a + (c + b)
  rw [Nat.add_comm b c]
  -- Rewritten to: a + (c + b) = a + (c + b),
  -- then solved automatically by reflexivity.

-- Use `at h` to rewrite a hypothesis, rather than the conclusion.
example (a b : Nat) (h : 0 + a = b) : a = b := by
  --> Goal: h : 0 + a = b ⊢ a = b
  rw [Nat.zero_add] at h
  --> Goal: h : a = b ⊢ a = b
  assumption

-- ----------------------------------------------------
-- * Decidability

-- There is a very useful tactic called `decide`, which deserves
-- a lecture of its own. Let's start with an example.

-- Here, `7 ∣ 343` means `7` is a divisor of `343`. Use
-- `\|` to type this symbol (it's a different Unicode
-- character than plain `|`).
example : 7 ∣ 343 := by
  decide

-- The `decide` tactic can be used to prove
-- propositions that are "decidable". But what makes a
-- proposition decidable, and how is it decided?

-- It would be awesome to have a function
-- `decide : Prop → Bool` that simply told you whether
-- a theorem is true or false. But of course, as
-- discussed on the homework, we cannot have this.

-- Instead, Lean defined a type class `Decidable` which
-- contains decidable propositions. You can even define
-- your own instances for it!

-- Let's figure out how one would do this, in several
-- steps.

namespace Attempt1

  -- In class, Lean was constantly complaining because
  -- our names clashed with names from the standard
  -- library. So I am modifying these lecture notes
  -- by adding "X" everywhere.

  -- As a first attempt, let's define a type class
  -- `DecidableX` for propositions. Propositions `α`
  -- that belong to this type class are equipped with a
  -- boolean, called `decideX α`, with the intention
  -- that the boolean is `true` is `α` is true, and
  -- `false` if it is false.
  class DecidableX (α : Prop) where
    decideX : Bool

  open DecidableX

  #check @decideX --> decideX : (α : Prop) → [self : DecidableX α] → Bool

  -- Note that the type of `decideX` is almost `Prop → Bool`, but
  -- with the caveat that the proposition must be decidable.

  -- Let's define a predicate for evenness, as in Lecture 10.
  inductive EvenX : (n : Nat) → Prop where
    | zero_evenX : EvenX 0
    | n_plus_2_evenX : ∀ {n}, EvenX n → EvenX (n+2)

  -- We can easily define a function that decides, for
  -- a natural number `n`, whether it is even or not.
  -- The easiest way to do so is by recursion.
  def is_even : (n : Nat) → Bool
    | 0 => true
    | 1 => false
    | n+2 => is_even n

  #eval is_even 15      --> false
  #eval is_even 474     --> true

  -- Now we can define a `DecidableX` instance for `EvenX`:
  instance (n : Nat) : DecidableX (EvenX n) where
    decideX := is_even n

  -- Instead of calling `is_even` directly, we can now
  -- call the general-purpose `decideX` function.

  #eval decideX (EvenX 15)    --> false
  #eval decideX (EvenX 474)   --> true

  -- Problem: nobody checked if it's correct! We could
  -- have defined `is_even` incorrectly, for example like this:
  def is_evil : (n : Nat) → Bool
    | 0 => false
    | 1 => true
    | n+2 => is_evil n

  -- If we use `is_evil` instead of `is_even`,
  -- `decideX` will give an incorrect answer! To avoid
  -- this from happening, we should prove a theorem
  -- showing that the decider is correct:

  theorem even_decider_is_correct :
    ∀ n, EvenX n ↔ (decideX (EvenX n) = true) :=
    sorry

  -- We will not prove this theorem now, since there is
  -- a better way to do it.
end Attempt1

namespace Attempt2

  -- Instead of defining a function from `Prop` to
  -- `Bool` and then proving its correctness, let's
  -- define a better function: it inputs a proposition
  -- `α`, and it outputs a proof of `α` if `α` is true,
  -- and a proof of `¬α` if `α` is false (together with
  -- an indication of which of the two is the case).
  -- This is basically a boolean, but embellished with
  -- a proof.

  -- At first, ignore the word `class` in the following.
  -- `DecidableX α` is an inductive type with two
  -- constructors: `isTrueX` and `isFalseX`.

  class inductive DecidableX (α : Prop) where
    | isTrueX : α → DecidableX α
    | isFalseX : ¬α → DecidableX α

  open DecidableX

  -- The evenness predicate, again.
  inductive EvenX : (n : Nat) → Prop where
    | zero_evenX : EvenX 0
    | n_plus_2_evenX : ∀ {n}, EvenX n → EvenX (n+2)

  open EvenX

  -- The following theorems will be handy.

  -- `1` is not even.
  theorem not_even_one : ¬ (EvenX 1) := nofun
  -- `nofun` means `fun h => nomatch h`

  -- If `n+2` is even, then so is `n`.
  theorem n_minus_two_even : ∀ {n}, EvenX (n+2) → EvenX n :=
    fun h => match h with
      | n_plus_2_evenX h' => by assumption

  -- If `n` is odd, then so is `n+2`.
  theorem n_plus_two_not_even : ∀ {n}, ¬(EvenX n) → ¬(EvenX (n+2)) := by
    intros n h1 h2
    have h3 : EvenX n := by apply n_minus_two_even h2
    contradiction

  -- Now we can define our enhanced `is_even` function.
  -- Not only does it decide whether `n` is even or not,
  -- but it also preserves the evidence.

  def is_even : (n : Nat) → DecidableX (EvenX n)
    | 0 => isTrueX zero_evenX
    | 1 => isFalseX not_even_one
    | n+2 => match is_even n with
      | isTrueX h => isTrueX (n_plus_2_evenX h)
      | isFalseX h => isFalseX (n_plus_two_not_even h)

  -- Now for every `n`, `is_even` returns not only
  -- `true` or `false`, but also the evidence.
  #reduce is_even 0     --> isTrueX zero_evenX
  #reduce is_even 1     --> isFalseX not_even_one
  #reduce is_even 2     --> isTrueX (n_plus_2_evenX zero_evenX)
  #reduce is_even 3     --> isFalseX (n_plus_two_not_even not_even_one)

  -- Now we can define the `DecidableX` instance for `EvenX n`,
  -- which is just the same thing as the `is_even` function.
  instance even_is_decidable (n : Nat) : DecidableX (EvenX n) := is_even n

  -- Now we can write our own `decideX` function that returns a boolean:
  def decideX (α : Prop) [inst : DecidableX α] : Bool := match inst with
    | isTrueX _ => true
    | isFalseX _ => false

  -- Deciding evenness now works, just like in `Attempt`:
  #eval decideX (EvenX 0)     --> true
  #eval decideX (EvenX 1)     --> false
  #eval decideX (EvenX 2)     --> true

  -- The difference is that its correctness is provable
  -- once and for all.
  theorem decide_is_correct (α : Prop) [inst : DecidableX α] : decideX α = true ↔ α :=
    match inst with
    | isTrueX h => ⟨fun _ => h, fun _ => rfl⟩
    | isFalseX h => ⟨nofun, fun _ => by contradiction⟩

  -- The left-to-right direction is more useful, so we give it a special name:
  theorem of_decide (α : Prop) [DecidableX α] : decideX α = true → α :=
    (decide_is_correct α).1

  -- Now you can see how the `decide` tactic can be
  -- defined. We use `apply of_decide _` to reduce the
  -- goal to `decideX (EvenX 18) = true`, then use
  -- reflexivity to prove it. All the work is done
  -- by the decision procedure.
  example : EvenX 18 := by
    apply of_decide _ rfl

  -- --------------------------------------------------
  -- * Finding class instances

  -- As Jeremiah pointed out, the `#synth` command
  -- allows us to directly check whether there is a
  -- type class instance for a given type, and what it
  -- is.
  #synth DecidableX (EvenX 3) --> even_is_decidable 3

  -- Note that `even_is_decidable` is the name we chose
  -- for our instance on line 242. Had we not given it
  -- a name, Lean would have automatically named it
  -- `instDecidableXEvenX`.


end Attempt2
