fun_induction

fun_induction は、特定の再帰関数用の帰納法ができるようにします。

たとえば、自然数について帰納法を行うと n = 0 の場合と n = n' + 1 の場合に場合分けをすることになります。しかし、関数 f について何かを示そうとしているとき、f が自然数の再帰的構造に沿って定義されているとは限りません。そのような場合に fun_induction を使うと、場合分けの枝が一致しない問題と格闘しないで済みます。

/-- フィボナッチ数列の通常の定義をそのまま Lean の関数として書いたもの -/
@[simp]
def fibonacci (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fibonacci n + fibonacci (n + 1)

/-- フィボナッチ数列の線形時間の実装 -/
def fib (n : Nat) : Nat :=
  (loop n).1
where
  loop (x : Nat) : Nat × Nat :=
    match x with
    | 0 => (0, 1)
    | n + 1 =>
      let p := loop n
      (p.2, p.1 + p.2)

@[simp]
theorem fib_zero : fib 0 = 0 := by rfl

@[simp]
theorem fib_one : fib 1 = 1 := by rfl

/-- `fib` が `fibonacci` と同じ漸化式を満たす -/
@[simp]
theorem fib_add (n : Nat) : fib n + fib (n + 1) = fib (n + 2) := by rfl

/-- `fibonacci` と `fib` は同じ結果を返す -/
example (n : Nat) : fibonacci n = fib n := by
  fun_induction fibonacci n with
  | case1 => rfl
  | case2 => simp
  | case3 n ih1 ih2 =>
    simp [ih1, ih2]

induction タクティクと同様に、with の後にタクティクを続けると、すべての枝に対してそのタクティクを適用します。

example (n : Nat) : fibonacci n = fib n := by
  fun_induction fibonacci n with simp_all

舞台裏

再帰関数 foo を定義すると、裏で Lean が帰納原理(induction principle) foo.inductfoo.induct_unfolding を生成します。

/-- フィボナッチ数列 -/
def fibonacci (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 1
  | n + 2 => fibonacci (n + 1) + fibonacci n

#check fibonacci.induct

#check fibonacci.induct_unfolding

帰納原理が生成されるのは再帰的な関数のみです。再帰的でない関数には生成されません。

def swapHead (l : List Nat) : List Nat :=
  match l with
  | [] => []
  | [x] => [x]
  | x :: y :: xs => y :: x :: xs

-- 帰納原理が生成されていない
#check_failure swapHead.induct

fun_induction タクティクは、この自動生成された foo.induct_unfolding を利用して帰納法を行っています。

帰納的述語への応用

帰納原理が自動生成されるのは再帰関数に対してだけで、帰納的述語に対しては生成されません。しかし、帰納的述語を再帰関数として書き直すことができるのであれば、その再帰関数に対して生成された *.induct 定理を使って帰納法を行うことができます。

これにより、(再帰関数として書き直せるような)帰納的述語に対しても、帰納法の枝が上手くハマらない問題を解決することができます。

/-- 回文を表す帰納的述語 -/
@[grind]
inductive Palindrome {α : Type} : List α → Prop
  /-- 空リストは回文 -/
  | nil : Palindrome []
  /-- 要素が一つだけのリストは回文 -/
  | single (a : α) : Palindrome [a]
  /-- 回文の両端に同じ要素を追加しても回文 -/
  | sandwich {a : α} {as : List α} (ih : Palindrome as) : Palindrome ([a] ++ as ++ [a])

variable {α : Type}


-- 普通に帰納法を使おうとすると、場合分けの枝がうまくはまらない
example (as : List α) (h : as.reverse = as) : Palindrome as := by
  induction as with
  | nil => grind
  | cons a as ih =>
    /-
    ih : as.reverse = as → Palindrome as
    h : (a :: as).reverse = a :: as
    ⊢ Palindrome (a :: as)
    -/

    -- 簡単には証明できない
    fail_if_success grind
    sorry

-- `α`に対して`(· = ·)`が決定可能という仮定がないため、
-- 古典論理を使用する
open scoped Classical in

/-- 回文判定を行う再帰関数。
`Palindrome` の定義になるべく忠実に書き直したもの -/
def PalindromeRec (as : List α) : Prop :=
  match as with
  | [] => True
  | [a] => True
  | a₁ :: a₂ :: as =>
    let xs := (a₂ :: as).dropLast
    let x := (a₂ :: as).getLast (by simp)

    -- 後に証明に使うときの利便性のために補題を示しておく
    have : [a₁] ++ xs ++ [x] = a₁ :: a₂ :: as := by
      grind [List.dropLast_concat_getLast]

    if a₁ = x then
      PalindromeRec xs
    else
      false
termination_by as.length

-- あっさり証明できる!
example (as : List α) (h : as.reverse = as) : Palindrome as := by
  induction as using PalindromeRec.induct with grind