decreasing_by

decreasing_by は、再帰関数などの停止性(計算が無限に続くことがなく、値が一意に決まること)を示し、Lean に定義を受け入れさせるために使われます。

使用例

たとえば、以下の関数はそのままでは Lean が停止性を示せないためエラーになってしまいます。

-- エラーになってしまう
/-
error: fail to show termination
-/
def Nat.toListNat (n : Nat) : List Nat :=
  if n == 0 then
    []
  else
    Nat.toListNat (n / 10) ++ [(n % 10)]

decreasing_by に続けて停止することの証明を与えれば Lean に受け入れられるようになります。再帰のたびに(無限には減少できない)引数が真に減少することを示せば十分です。

def Nat.toListNat (n : Nat) : List Nat :=
  if n == 0 then
    []
  else
    Nat.toListNat (n / 10) ++ [(n % 10)]
decreasing_by
  -- `n ≠ 0 → n / 10 < n` を示す
  guard_target =ₛ n / 10 < n

  grind

-- 動作テスト
#guard Nat.toListNat 1234 = [1, 2, 3, 4]

あるいは、decreasing_by を使用しなくても、関数内で have などを使って停止性を証明しておいても良いです。

def Nat.toListNat (n : Nat) : List Nat :=
  if h : n == 0 then
    []
  else
    have : n / 10 < n := by
      grind
    Nat.toListNat (n / 10) ++ [(n % 10)]

部分関数を禁止するのはなぜか?

そもそもなぜ Lean では関数の停止性を示す必要があるのでしょうか?それは、Lean の論理体系の健全性を保つためです。1

カリー・ハワード同型対応によれば「帰納法は再帰」なので、停止しない再帰を許すことは直接的に「終了しない帰納法」を許すことに繋がり、矛盾をもたらすことになってしまいます。

-- unsafe を使うと終わらない帰納法が使えるので、何でも証明できる
unsafe def loop_thm (P : Prop) : P :=
  loop_thm P

unsafe example : False := loop_thm False

定理ではなくて通常の関数の場合も、停止しない関数を野放図に許すと矛盾を導くことができます。例として、以下の「Option 値の点列から some であるようなものを探す関数」を考えてみましょう。この関数は引数の点列の f がいつか some になる保証がないので、無限に計算が終わらない可能性があります。

variable {α : Type}

/-- 点列 `f : Nat → Option α` が `some` を返すような最初の `f n` を返す -/
unsafe def search (f : Nat → Option α) (start : Nat) : α :=
  match f start with
  | .some x => x
  | .none => search f (start + 1)

ここで問題なのは、この関数 search を使うと任意の型の項が構成できてしまうということです。

-- `α` は任意なので、どんな型の項でも作れることになる
unsafe def anything : α := search (fun _ => none) 0

したがって特に Empty 型の項を作ることもできて、カリー・ハワード同型対応により「型の項を作る」ことは「命題を証明する」ことに対応するので、矛盾が導かれたことになってしまいます。

unsafe example : False := by
  have _ : Empty := anything
  contradiction

  1. ここで紹介している例は Joachim Breitner さんによる Recursive definitions in Lean というブログ記事から引用しています。