Copyright © 2018–2023 Anne Baanen, Alexander Bentkamp, Jasmin Blanchette, Johannes Hölzl, and Jannis Limperg. See LICENSE.txt.

import LoVe.Lectures.LoVe05_FunctionalProgramming_Demo

LoVe Demo 6: Inductive Predicates

Inductive predicates, or inductively defined propositions, are a convenient way to specify functions of type ⋯ → Prop. They are reminiscent of formal systems and of the Horn clauses of Prolog, the logic programming language par excellence.

A possible view of Lean:

Lean = functional programming + logic programming + more logic
namespace LoVe

Introductory Examples

Even Numbers

Mathematicians often define sets as the smallest that meets some criteria. For example:

The set `E` of even natural numbers is defined as the smallest set closed
under the following rules: (1) `0 ∈ E` and (2) for every `k ∈ ℕ`, if
`k ∈ E`, then `k + 2 ∈ E`.

In Lean, we can define the corresponding "is even" predicate as follows:

inductive Even : ℕ → Prop where
  | zero    : Even 0
  | add_two : ∀k : ℕ, Even k → Even (k + 2)

This should look familiar. We have used the same syntax, except with Type instead of Prop, for inductive types.

The above command introduces a new unary predicate Even as well as two constructors, Even.zero and Even.add_two, which can be used to build proof terms. Thanks to the "no junk" guarantee of inductive definitions, Even.zero and Even.add_two are the only two ways to construct proofs of Even.

By the PAT principle, Even can be seen as an inductive type, the values being the proof terms.

theorem Even_4 :
  Even 4 :=
  Even.add_two 2 (Even.add_two 0 Even.zero)

Why cannot we simply define Even recursively? Indeed, why not?

def evenRec : ℕ → Bool
  | 0     => true
  | 1     => false
  | k + 2 => evenRec k

There are advantages and disadvantages to both styles.

The recursive version requires us to specify a false case (1), and it requires us to worry about termination. On the other hand, because it is computational, it works well with rfl, simp, #reduce, and #eval.

The inductive version is often considered more abstract and elegant. Each rule is stated independently of the others.

Yet another way to define Even is as a nonrecursive definition:

def evenNonrec (k : ℕ) : Prop :=
  k % 2 = 0

Mathematicians would probably find this the most satisfactory definition. But the inductive version is a convenient, intuitive example that is typical of many realistic inductive definitions.

Tennis Games

Transition systems consists of transition rules, which together specify a binary predicate connecting a "before" and an "after" state. As a simple specimen of a transition system, we consider the possible transitions, in a game of tennis, starting from 0–0.

inductive Score : Type where
  | vs       : ℕ → ℕ → Score
  | advServ  : Score
  | advRecv  : Score
  | gameServ : Score
  | gameRecv : Score

infixr:50 " – " => Score.vs

inductive Step : Score → Score → Prop where
  | serv_0_15     : ∀n, Step (0–n) (15–n)
  | serv_15_30    : ∀n, Step (15–n) (30–n)
  | serv_30_40    : ∀n, Step (30–n) (40–n)
  | serv_40_game  : ∀n, n < 40 → Step (40–n) Score.gameServ
  | serv_40_adv   : Step (40–40) Score.advServ
  | serv_adv_40   : Step Score.advServ (40–40)
  | serv_adv_game : Step Score.advServ Score.gameServ
  | recv_0_15     : ∀n, Step (n–0) (n–15)
  | recv_15_30    : ∀n, Step (n–15) (n–30)
  | recv_30_40    : ∀n, Step (n–30) (n–40)
  | recv_40_game  : ∀n, n < 40 → Step (n–40) Score.gameRecv
  | recv_40_adv   : Step (40–40) Score.advRecv
  | recv_adv_40   : Step Score.advRecv (40–40)
  | recv_adv_game : Step Score.advRecv Score.gameRecv


#check Step (11–16) (15–3)

infixr:45 " ↝ " => Step

Note that while Score.vs allows arbitrary numbers as arguments, the formulation of the constructors for Step ensures only valid tennis scores can be reached from 0–0.

We can ask, and formally answer, questions such as: Can the score ever return to 0–0?

theorem no_Step_to_0_0 (s : Score) :
  ¬ s ↝ 0–0 := by
    intro h
    cases h

Reflexive Transitive Closure

Our last introductory example is the reflexive transitive closure of a relation R, modeled as a binary predicate Star R.

inductive Star {α : Type} (R : α → α → Prop) : α → α → Prop
where
  | base (a b : α)    : R a b → Star R a b
  | refl (a : α)      : Star R a a
  | trans (a b c : α) : Star R a b → Star R b c → Star R a c

The first rule embeds R into Star R. The second rule achieves the reflexive closure. The third rule achieves the transitive closure.

The definition is truly elegant. If you doubt this, try implementing Star as a recursive function:

def starRec {α : Type} (R : α → α → Bool) :
  α → α → Bool :=
  sorry

theorem s1 : Star Step (0–0) Score.gameServ := by
  repeat
    apply Star.trans
    { apply Star.base
      constructor }
  apply Star.base
  constructor
  norm_num

theorem s2 : Star Step (0–0) Score.gameServ := by
  apply Star.trans
  { apply Star.base
    constructor }
  { apply Star.trans
    { apply Star.base
      apply Step.recv_0_15 }
    { apply Star.trans
      { apply Star.base
        constructor }
      { apply Star.trans
        { apply Star.base
          constructor }
        { apply Star.base
          constructor
          norm_num } } } }

example : s1 = s2 := rfl

-- proof irrelevance: this is what we have in Lean
example (P : Prop) (p1 p2 : P) : p1 = p2 := rfl

-- uniqueness of identity proofs
example (α : Type) (a1 a2 : α) (p1 p2 : a1 = a2) : p1 = p2 := rfl

-- non-definitional proof irrelevance
example (P : Prop) (p1 p2 : P) : p1 = p2 := proof_irrel _ _

A Nonexample

Not all inductive definitions are legal.

-- fails
-- inductive Illegal : Prop where
--   | intro : ¬ Illegal → Illegal

Logical Symbols

The truth values False and True, the connectives , and , the quantifier, and the equality predicate = are all defined as inductive propositions or predicates. In contrast, and are built into the logic.

Syntactic sugar:

`∃x : α, P` := `Exists (λx : α, P)`
`x = y`     := `Eq x y`
namespace logical_symbols

inductive And (a b : Prop) : Prop where
  | intro : a → b → And a b

inductive Or (a b : Prop) : Prop where
  | inl : a → Or a b
  | inr : b → Or a b

inductive Iff (a b : Prop) : Prop where
  | intro : (a → b) → (b → a) → Iff a b

inductive Exists {α : Type} (P : α → Prop) : Prop where
  | intro : ∀a : α, P a → Exists P

inductive True : Prop where
  | intro : True

inductive False : Prop where

inductive Eq {α : Type} : α → α → Prop where
  | refl : ∀a : α, Eq a a


end logical_symbols

#print And
#print Or
#print Iff
#print Exists
#print True
#print False
#print Eq

Rule Induction

Just as we can perform induction on a term, we can perform induction on a proof term.

This is called rule induction, because the induction is on the introduction rules (i.e., the constructors of the proof term). Thanks to the PAT principle, this works as expected.

theorem mod_two_Eq_zero_of_Even (n : ℕ) (h : Even n) :
  n % 2 = 0 := by
    induction h with
    | zero => rfl
    | add_two k hk ih => simp [ih]

theorem Not_Even_two_mul_add_one (m n : ℕ)
    (hm : m = 2 * n + 1) :
  ¬ Even m := by
    intro h
    induction h generalizing n with
    | zero => cases hm
    | add_two k hk ih =>
      apply ih (n - 1)
      cases n with
      | zero => simp at hm
      | succ n =>
        simp at hm
        simp
        linarith

linarith proves goals involving linear arithmetic equalities or inequalities. "Linear" means it works only with + and -, not * and / (but multiplication by a constant is supported).

theorem linarith_example (i : Int) (hi : i > 5) :
  2 * i + 3 > 11 :=
  by linarith

theorem Star_Star_Iff_Star {α : Type} (R : α → α → Prop)
    (a b : α) :
  Star (Star R) a b ↔ Star R a b :=
  sorry

@[simp] theorem Star_Star_Eq_Star {α : Type}
    (R : α → α → Prop) :
  Star (Star R) = Star R :=
  by
    apply funext
    intro a
    apply funext
    intro b
    apply propext
    apply Star_Star_Iff_Star

#check funext
#check propext

Elimination

Given an inductive predicate P, its introduction rules typically have the form ∀…, ⋯ → P … and can be used to prove goals of the form ⊢ P ….

Elimination works the other way around: It extracts information from a theorem or hypothesis of the form P …. Elimination takes various forms: pattern matching, the cases and induction tactics, and custom elimination rules (e.g., And.left).

Now we can finally understand how cases h where h : l = r and how cases Classical.em h work.

#print Eq

theorem cases_Eq_example {α : Type} (l r : α) (h : l = r)
    (P : α → α → Prop) :
  P l r :=
  by
    cases h
    sorry

#check Classical.em
#print Or

theorem cases_Classical_em_example {α : Type} (a : α)
    (P Q : α → Prop) :
  Q a :=
  by
    have hor : P a ∨ ¬ P a :=
      Classical.em (P a)
    cases hor with
    | inl hl => sorry
    | inr hr => sorry

Further Examples

Sorted Lists

inductive Sorted : List ℕ → Prop where
  | nil : Sorted []
  | single (x : ℕ) : Sorted [x]
  | two_or_more (x y : ℕ) {zs : List ℕ} (hle : x ≤ y)
      (hsorted : Sorted (y :: zs)) :
    Sorted (x :: y :: zs)

theorem Sorted_nil :
  Sorted [] :=
  Sorted.nil

theorem Sorted_2 :
  Sorted [2] :=
  Sorted.single _

theorem Sorted_3_5 :
  Sorted [3, 5] :=
  by
    apply Sorted.two_or_more
    { simp }
    { exact Sorted.single _ }

theorem Sorted_3_5_raw :
  Sorted [3, 5] :=
  Sorted.two_or_more _ _ (by simp) (Sorted.single _)

theorem sorted_7_9_9_11 :
  Sorted [7, 9, 9, 11] :=
  Sorted.two_or_more _ _ (by simp)
    (Sorted.two_or_more _ _ (by simp)
       (Sorted.two_or_more _ _ (by simp)
          (Sorted.single _)))

theorem Not_Sorted_17_13 :
  ¬ Sorted [17, 13] :=
  by
    intro h
    cases h with
    | two_or_more _ _ hlet hsorted => simp at hlet

Palindromes

inductive Palindrome {α : Type} : List α → Prop where
  | nil : Palindrome []
  | single (x : α) : Palindrome [x]
  | sandwich (x : α) (xs : List α) (hxs : Palindrome xs) :
    Palindrome ([x] ++ xs ++ [x])


-- fails

def palindromeRec {α : Type} : List α → Bool | [] => true | [_] => true | ([x] ++ xs ++ [x]) => palindromeRec xs | _ => false

theorem Palindrome_aa {α : Type} (a : α) :
  Palindrome [a, a] :=
  Palindrome.sandwich a _ Palindrome.nil

theorem Palindrome_aba {α : Type} (a b : α) :
  Palindrome [a, b, a] :=
  Palindrome.sandwich a _ (Palindrome.single b)

theorem Palindrome_reverse {α : Type} (xs : List α)
    (hxs : Palindrome xs) :
  Palindrome (reverse xs) :=
  by
    induction hxs with
    | nil                  => exact Palindrome.nil
    | single x             => exact Palindrome.single x
    | sandwich x xs hxs ih =>
      { simp [reverse, reverse_append]
        exact Palindrome.sandwich _ _ ih }

Full Binary Trees

#check BTree

inductive IsFull {α : Type} : BTree α → Prop where
  | empty : IsFull BTree.empty
  | node (a : α) (l r : BTree α)
      (hl : IsFull l) (hr : IsFull r)
      (hiff : l = BTree.empty ↔ r = BTree.empty) :
    IsFull (BTree.node a l r)

theorem IsFull_singleton {α : Type} (a : α) :
  IsFull (BTree.node a BTree.empty BTree.empty) :=
  by
    apply IsFull.node
    { exact IsFull.empty }
    { exact IsFull.empty }
    { rfl }

theorem IsFull_mirror {α : Type} (t : BTree α)
    (ht : IsFull t) :
  IsFull (mirror t) :=
  by
    induction ht with
    | empty                           => exact IsFull.empty
    | node a l r hl hr hiff ih_l ih_r =>
      { rw [mirror]
        apply IsFull.node
        { exact ih_r }
        { exact ih_l }
        { simp [mirror_Eq_empty_Iff, *] } }

theorem IsFull_mirror_struct_induct {α : Type} (t : BTree α) :
  IsFull t → IsFull (mirror t) :=
  by
    induction t with
    | empty                =>
      { intro ht
        exact ht }
    | node a l r ih_l ih_r =>
      { intro ht
        cases ht with
        | node _ _ _ hl hr hiff =>
          { rw [mirror]
            apply IsFull.node
            { exact ih_r hr }
            { apply ih_l hl }
            { simp [mirror_Eq_empty_Iff, *] } } }

First-Order Terms

inductive Term (α β : Type) : Type where
  | var : β → Term α β
  | fn  : α → List (Term α β) → Term α β

inductive WellFormed {α β : Type} (arity : α → ℕ) :
  Term α β → Prop where
  | var (x : β) : WellFormed arity (Term.var x)
  | fn (f : α) (ts : List (Term α β))
      (hargs : ∀t ∈ ts, WellFormed arity t)
      (hlen : length ts = arity f) :
    WellFormed arity (Term.fn f ts)

inductive VariableFree {α β : Type} : Term α β → Prop where
  | fn (f : α) (ts : List (Term α β))
      (hargs : ∀t ∈ ts, VariableFree t) :
    VariableFree (Term.fn f ts)

end LoVe