5.3. 整礎再帰(Well-Founded Recursion)🔗

整礎再帰 (well-founded recursion)によって定義される関数とは、各再帰呼び出しがその関数のパラメータよりも( 適切な意味 において) 小さい 引数を持つものを差します。 構造的再帰 では再帰定義は特定の 構文的 要件を満たさなければならないことと対照的に、整礎再帰を使用する定義は 意味的 な引数を使用します。これにより、より多くのクラスの再帰定義を受け入れることができます。さらに、Lean の自動化が停止証明を構築できない場合、手動で停止証明を指定することができます。

すべての定義は Lean のコンパイラによって同じように扱われます。Lean の論理において、整礎再帰を使用した定義は通常、 definitionally に簡約されません。しかしこの簡約は propositional equality として成立しており、Lean は自動的にこれを証明します。このため、整礎再帰を使用した定義の性質を証明することが難しくなることは通常ありません。なぜなら propositional の簡約は関数の挙動を推論するために使用できるからです。しかし、これらの関数を型で使用することは一般的にうまく行かないことを意味します。仮に簡約の挙動が definitionally に成立する場合であっても、カーネル内での構造的再帰に比べると、定義と共に停止の証明を展開しなければならないため、かなり遅くなることが多いです。可能であれば、型や definitional equality が重要な状況での使用を意図した再帰関数は、構造的再帰で定義されるべきです。

整礎再帰を明示的に使用するには、関数または定理の定義に Lean.Parser.Command.declaration : commandtermination_by 句を注釈して、関数の停止のための 測度 (measure)を指定します。この測度は、再帰呼び出しのたびに減少する項でなければなりません;これは関数のパラメータのうちのどれか1つ、またはパラメータのタプルである場合もありますが、それ以外の項である場合もあります。測度の型には 整礎関係 を指定する必要があります。これは測度の減少の意味を決定します。

syntaxExplicit Well-Founded Recursion

Lean.Parser.Command.declaration : commandtermination_by 句は停止引数を導入します。

Specify a termination argument for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination argument will be inferred. If written as `termination_by?`,
the inferrred termination argument will be suggested.

terminationBy ::= ...
    | Specify a termination argument for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination argument will be inferred. If written as `termination_by?`,
the inferrred termination argument will be suggested.

termination_by (ident* =>)? term

オプションの => の前の識別子には、宣言ヘッダでまだ束縛されていない関数のパラメータをスコープに入れることができます。必須の項には関数のパラメータの1つを指定しなければなりません。これにはヘッダもしくは句の中で局所的に導入されたもののどちらでも利用できます。

引き算の繰り返しによる割り算

割り算は被除数から除数を引く回数として指定できます。引き算はパターンマッチではないため、この演算を構造的再帰を使用してエラボレートすることはできません。n の値は再帰呼び出しのたびに減少するので、引き算の繰り返しによる除算の定義を正当化するために、整礎再帰を使用することができます。

def div (n k : Nat) : Nat := if k = 0 then 0 else if k > n then 0 else 1 + div (n - k) k termination_by n

5.3.1. 整礎関係(Well-Founded Relations)🔗

関係 整礎関係 (well-founded relation)であるとは、無限降鎖が存在しないことを指します。

x_0 ≻ x_1 ≻ \cdots

Lean では、標準的な整礎関係を持つ型は WellFoundedRelation 型クラスのインスタンスです。

🔗type class
WellFoundedRelation.{u} (α : Sort u)
    : Sort (max 1 u)

Instance Constructor

WellFoundedRelation.mk.{u}

Methods

rel : ααProp
wf : WellFounded WellFoundedRelation.rel

最も重要なインスタンスは以下です:

  • Nat 、これは (· < ·) で順序付けられます。

  • Prod 、辞書式順序で順序付けられます: (a₁, b₁) (a₂, b₂) であるのは a₁ a₂ もしくは a₁ = a₂ かつ b₁ b₂ である場合に限ります。

  • SizeOf 型クラスのインスタンスであり、 SizeOf.sizeOf メソッドを提供するすべての型は整礎関係を持ちます。これらの型では、 x₁ x₂ であるのは sizeOf x₁ < sizeOf x₂ である場合に限ります。 帰納型 では、 SizeOf インスタンスは Lean によって自動的に導出されます。

SizeOf インスタンスとして任意の型に対して低い優先度で instSizeOfDefault インスタンスが提供されており、これは常に 0 を返すことに注意してください。 0 < 0 が偽であるため、このインスタンスによる整礎関係を使用して関数が終了することを証明するために使用することができません。

デフォルトの Size インスタンス

一般的な関数型は、停止証明に有用な整礎関係を持ちません。このため、 インスタンス統合instSizeOfDefault と対応する整礎関係を選択します。測度が関数の場合、デフォルトの SizeOf インスタンスが選択されるため以下の証明は成功しません。

def declaration uses 'sorry'fooInst (b : Bool Bool) : Unit := fooInst (b b) termination_by b decreasing_by b:BoolBoolsizeOf (bb) < sizeOf b b:BoolBool0 < 0 b:BoolBool0 < 0 b:BoolBoolFalse b:BoolBoolFalse All goals completed! 🐙

5.3.2. 停止証明(Termination proofs)

測度 が指定され、その 整礎関係 が決定されると、Lean はすべての再帰呼び出しの停止証明義務を決定します。

各再帰呼び出しの証明義務は g a₁ a₂ g p₁ p₂ の形式となります:

  • g はこれらのパラメータの関数としての測度です。

  • は推論される整礎関係です。

  • a₁ a₂ は再帰呼び出しの引数であり、

  • p₁ p₂ は関数定義のパラメータです。

この証明義務のコンテキストは、再帰呼び出しのローカルコンテキストです。具体的には、ローカルの仮定(if h : _match h : _ with have などで導入されるものなど)が利用できます。関数のパラメータが(例えば Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 式による)パターンマッチの 判別子 である場合、このパラメータは証明義務の中でマッチしたパターンに絞り込まれます。

停止の証明義務の全体は、各再帰呼び出しに対する1つのゴールで構成されます。デフォルトでは、タクティク decreasing_trivial が各証明義務の証明に使用されます。オプションの Lean.Parser.Command.declaration : commanddecreasing_by 句を使用することでカスタムのタクティクスクリプトを提供することができます。この句は Lean.Parser.Command.declaration : commandtermination_by 句の後に来ます。このタクティクスクリプトは証明義務ごとに個別に実行されるのではなく、証明義務ごとに1つのゴールで1回実行されます。

停止の証明義務

以下のフィボナッチ数の再帰的定義には2つの再帰的呼び出しがあり、これにより停止の証明には2つのゴールがあります。

def fib (n : Nat) := if h : n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n unsolved goals n : Nat h : ¬n ≤ 1 ⊢ n - 1 < n n : Nat h : ¬n ≤ 1 ⊢ n - 2 < ndecreasing_by n:Nath:¬n1n - 1 < nn:Nath:¬n1n - 2 < n
n:Nath:¬n1n - 1 < nn:Nath:¬n1n - 2 < n

ここで、 測度 は単にパラメータそのものであり、整礎順序は自然数の小なりの関係です。最初の証明のゴールでは、最初の再帰呼び出しの引数、すなわち n - 1 が関数のパラメータ n より狭義に小さいことを証明することを要求します。

どちらの停止の証明も、 omega タクティクを使って簡単に解消することができます。

def fib (n : Nat) := if h : n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n decreasing_by n:Nath:¬n1n - 1 < n All goals completed! 🐙 n:Nath:¬n1n - 2 < n All goals completed! 🐙
パラメータの絞り込み

関数のパラメータがパターンマッチの 判別子 である場合、証明義務は絞り込まれたパラメータに言及します。

def fib : Nat Nat | 0 | 1 => 1 | .succ (.succ n) => fib (n + 1) + fib n termination_by n => n unsolved goals n : Nat ⊢ n + 1 < n.succ.succ n : Nat ⊢ n < n.succ.succdecreasing_by n:Natn + 1 < n.succ.succn:Natn < n.succ.succ
n:Natn + 1 < n.succ.succn:Natn < n.succ.succ

さらに、 if-then-else 式の分岐では、依存 if-then-else 構文を使用した場合と同様に、現在の分岐の条件を主張する仮定がスコープに入ります。

ローカルの仮定と条件

ここで、 termIfThenElse : term`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to return `t` or `e` depending on whether `c` is true or false. The explicit argument `c : Prop` does not have any actual computational content, but there is an additional `[Decidable c]` argument synthesized by typeclass inference which actually determines how to evaluate `c` to true or false. Write `if h : c then t else e` instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact that `c` is true/false. if は条件についてのローカルの仮定をスコープに入れません。とはいえ、停止証明のコンテキストでは利用可能です。

def fib (n : Nat) := if n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n unsolved goals n : Nat h✝ : ¬n ≤ 1 ⊢ n - 1 < n n : Nat h✝ : ¬n ≤ 1 ⊢ n - 2 < ndecreasing_by n:Nath✝:¬n1n - 1 < nn:Nath✝:¬n1n - 2 < n
n:Nath✝:¬n1n - 1 < nn:Nath✝:¬n1n - 2 < n
高階関数の入れ子の再帰

再帰呼び出しが高階関数に入れ子になっている場合、停止の証明義務が果たせるように関数定義を調整しなければならないことがあります。これは以下の木構造のように 入れ子の帰納型 上で再帰的に関数を定義する際によく発生します:

structure Tree where children : List Tree

素朴にこのデータ構造に対して再帰関数を定義しようとすると失敗します:

def Tree.depth (t : Tree) : Nat := let depths := t.children.map (fun c => failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal t c : Tree ⊢ sizeOf c < sizeOf tTree.depth c) match depths.max? with | some d => d+1 | none => 0 termination_by t
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
t c : Tree
⊢ sizeOf c < sizeOf t

ローカル変数 c とパラメータ t の間に関連がないため、この停止の証明義務は証明できません。ただし、 List.map は与えられたリストの要素にのみ関数の引数を適用します;そのため c は常に t.children の要素です。

停止証明のゴールは再帰呼び出しのローカルコンテキストへのアクセスを提供するため、 c が確かにリスト t.children の要素であることを示す事実を関数定義のスコープに入れることに役立ちます。これは、関数 List.attach を使って t.children の各要素にメンバシップの証明を「アタッチ」し、 List.map に渡された関数のスコープにこの証明を取り込むことで実現できます:

def Tree.depth (t : Tree) : Nat := let depths := t.children.attach.map (fun c, hc => Tree.depth c) match depths.max? with | some d => d+1 | none => 0 termination_by t decreasing_by t:Treec:Treehc:ct.childrensizeOf c < sizeOf t

Lean.Parser.Command.declaration : commanddecreasing_by の後の証明ゴールには c t.children という仮定が含まれるようになり、 decreasing_tactic が成功すれば十分であることに注意してください。

5.3.3. デフォルトの停止証明タクティク(Default Termination Proof Tactic)

Lean.Parser.Command.declaration : commanddecreasing_by 句が与えられない場合、 decreasing_tactic が暗黙的に使用され、各証明義務に個別に適用されます。

🔗tactic
decreasing_tactic

タクティク decreasing_tactic は主にタプルの辞書式順序を扱い、直積の左成分が definitionally equal である場合には Prod.Lex.right を、そうでない場合は Prod.Lex.left を適用します。このようにタプルを前処理した後、 decreasing_trivial タクティクを呼び出します。

🔗tactic
decreasing_trivial

Extensible helper tactic for decreasing_tactic. This handles the "base case" reasoning after applying lexicographic order lemmas. It can be extended by adding more macro definitions, e.g.

macro_rules | `(tactic| decreasing_trivial) => `(tactic| linarith)

タクティク decreasing_trivial は拡張可能なタクティクであり、いくつかの一般的なヒューリスティックスを適用して停止ゴールを解きます。特に、以下のタクティクと定理を試行します:

  • simp_arith

  • assumption

  • 定理 Nat.sub_succ_lt_selfNat.pred_lt'Nat.pred_lt 、これらは共通の算術的ゴールを扱います

  • omega

  • array_get_decarray_mem_dec 、これは配列の要素のサイズが配列のサイズより小さいことを証明するものです

  • sizeOf_list_dec 、リストの要素のサイズがリストのサイズ未満であること

  • String.Iterator.sizeOf_next_lt_of_hasNextString.Iterator.sizeOf_next_lt_of_atEnd 、これは Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass. `break` and `continue` are supported inside `for` loops. `for x in e, x2 in e2, ... do s` iterates of the given collections in parallel, until at least one of them is exhausted. The types of `e2` etc. must implement the `ToStream` typeclass. for を用いた文字列に対する繰り返しを扱います

このタクティクは Lean.Parser.Command.macro_rules : commandmacro_rules を使用してヒューリスティックスをさらに拡張することを意図しています。

辞書式順序のバックトラック無し

より複雑な 測度 を必要とする再帰関数の典型的な例としてアッカーマン関数が挙げられます:

def ack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => ack m 1 | m + 1, n + 1 => ack m (ack (m + 1) n) termination_by m n => (m, n)

ここでの測度はタプルであるため、すべての再帰呼び出しはパラメータよりも辞書式に小さい引数でなければなりません。これはデフォルトの decreasing_tactic で扱うことができます。

特に、3番目の再帰呼び出しは2番目のパラメータよりも小さい2番目の引数と、1番目のパラメータに definitionally equal な1番目の引数を持つ点に注意してください。これにより、 decreasing_tacticProd.Lex.right を適用することが可能になっています。

Prod.Lex.right {α β} {ra : α α Prop} {rb : β β Prop} (a : α) {b₁ b₂ : β} (h : rb b₁ b₂) : Prod.Lex ra rb (a, b₁) (a, b₂)

しかし、3番目の再帰呼び出しの第1引数が1番目のパラメータ以下であることが証明できるものの構文的には等しくないように修正された次の関数定義では失敗します:

def synack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => synack m 1 | m + 1, n + 1 => synack m (failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal case h m n : Nat ⊢ m / 2 + 1 < m + 1synack (m / 2 + 1) n) termination_by m n => (m, n)
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
case h
m n : Nat
⊢ m / 2 + 1 < m + 1

Prod.Lex.right が適用できないため、タクティクは Prod.Lex.left を使用し、その結果上記の証明不可能なゴールが生まれています。

この関数定義では、より一般的な定理 Prod.Lex.right' を使用した手作業による証明が必要になる場合があります。この定理では、タプルの最初のコンポーネント(これは Nat 型でなければなりません)が狭義な等しさではなく未満であることを許容します:

Prod.Lex.right' {β} (rb : β β Prop) {a₂ : Nat} {b₂ : β} {a₁ : Nat} {b₁ : β} (h₁ : a₁ a₂) (h₂ : rb b₁ b₂) : Prod.Lex Nat.lt rb (a₁, b₁) (a₂, b₂)def synack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => synack m 1 | m + 1, n + 1 => synack m (synack (m / 2 + 1) n) termination_by m n => (m, n) decreasing_by m:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, 1) (m.succ, 0) m:Natm < m.succ All goals completed! 🐙 -- the next goal corresponds to the third recursive call m:Natn:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m / 2 + 1, n) (m.succ, n.succ) m:Natn:Natm / 2 + 1m.succm:Natn:Natn < n.succ m:Natn:Natm / 2 + 1m.succ All goals completed! 🐙 m:Natn:Natn < n.succ All goals completed! 🐙 m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) → (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 ym.succ, n.succ⟩ → NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, x✝m / 2 + 1, n⟩ ⋯) (m.succ, n.succ) m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) → (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 ym.succ, n.succ⟩ → Natm < m.succ All goals completed! 🐙

decreasing_tactic タクティクは失敗時にバックトラックが必要になるため、より強力な Prod.Lex.right' を使用しません。

5.3.4. 整礎再帰の推測(Inferring Well-Founded Recursion)🔗

再帰関数の定義で停止のための 測度 を指定しない場合、Lean は自動的に停止性を発見しようとします。 Lean.Parser.Command.declaration : commandtermination_byLean.Parser.Command.declaration : commanddecreasing_by のどちらも指定されていない場合、Lean は整礎再帰を試みる前に 構造的再帰の推測 を試みます。 Lean.Parser.Command.declaration : commanddecreasing_by 句がある場合、整礎再帰のみが試みられます。

適切な停止のための 測度 を推論するために、Lean は 基本停止測度 (basic termination measure)を複数考慮します。これは Nat 型の停止のための測度です。Lean はこれらの測度のすべてのタプルを試します。

基本停止測度としては以下が考慮されます:

  • 非自明な SizeOf インスタンスを持つすべてのパラメータ

  • 再帰呼び出しのローカルコンテキストにおいて e₁ < e₂ または e₁ ≤ e₂ 型の仮定を持つ場合は式 e₂ - e₁ 。ここで e₁e₂Nat 型であり、関数のパラメータのみに依存します。 このアプローチは Panagiotis Manolios and Daron Vroon, 2006. “Termination Analysis with Calling Context Graphs”. In Proceedings of the International Conference on Computer Aided Verification (CAV 2006). (LNCS 4144) の成果に基づいています。

  • 相互グループにおいて、グループ内の他の関数への再帰呼び出しと、定義されている関数自身への再帰呼び出しを区別するために、追加の基本測度が使用されます。(詳細については 相互整礎再帰の節 を参照してください)。

測度候補 (candidate measure)は基本測度または基本測度のタプルです。測度候補のいずれかが停止証明タクティク(つまり Lean.Parser.Command.declaration : commanddecreasing_by で指定されるタクティク、もしくは Lean.Parser.Command.declaration : commanddecreasing_by 句が無い場合は decreasing_trivial で指定されるタクティク)によってすべての証明義務を解消できる場合、そのような任意の測度候補が自動停止測度として選択されます。

termination_by? 句を指定すると、推論された停止の注釈が表示されます。これは提案に対するサジェスト、またはコードアクションを使用してソースファイルに自動的に追加することができます。

測度についてすべてのタプルを試行することによる組み合わせ爆発を避けるために、Lean はまず 基本停止測度 を集計し、基本測度が減少的・狭義に減少的・非減少的のどれであるかを決定します。減少的な測度とは少なくとも1つの再帰呼び出しに対して小さくなり、任意の再帰呼び出しにおいて決して増加しないものを指します。一方、狭義に減少的な測度はすべての再帰呼び出しに対して小さくなります。非減少的な測度は停止タクティクが減少または狭義に減少することを示せないような測度です。こうして得られたテーブルに基づいて適切なタプルが選択されます。 このアプローチは Lukas Bulwahn, Alexander Krauss, and Tobias Nipkow, 2007. “Finding Lexicographic Orders for Termination Proofs in Isabelle/HOL”. In Proceedings of the International Conference on Theorem Proving in Higher Order Logics (TPHOLS 2007). (LNTCS 4732) に基づいています。 このテーブルは、測度が自動的に見つからなかった時のエラーメッセージに表示されます。

停止の失敗

Lean.Parser.Command.declaration : commandtermination_by 句が無い場合、Lean は整礎再帰のための測度を推測しようとします。失敗した場合、前述の表を出力します。この例では Lean.Parser.Command.declaration : commanddecreasing_by 句によって Lean の構造的再帰の推測が阻害されます;これによってエラーメッセージが特定されます。

Could not find a decreasing measure. The arguments relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) n m l 1) 38:6-25 = = = 2) 39:6-23 = < _ 3) 40:6-23 < _ _ Please use `termination_by` to specify a decreasing measure.def f : (n m l : Nat) Nat | n+1, m+1, l+1 => [ f (n+1) (m+1) (l+1), f (n+1) (m-1) (l), f (n) (m+1) (l) ].sum | _, _, _ => 0 decreasing_by all_goals decreasing_tactic
Could not find a decreasing measure.
The arguments relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
           n m l
1) 38:6-25 = = =
2) 39:6-23 = < _
3) 40:6-23 < _ _
Please use `termination_by` to specify a decreasing measure.

この3つの再帰呼び出しは、呼び出し元の位置によって識別されます。このメッセージは以下の事実を伝えています:

  • 最初の再帰呼び出しでは、すべての引数はパラメータと(証明的に)等価です。

  • 2回目の再帰呼び出しでは、第1引数は第1パラメータに等しく、第2引数は第2パラメータよりも証明的に小さいです。第3パラメータはこの再帰呼び出しではチェックされません。なぜなら適切な停止引数が存在しないことを判断する必要がないためです。

  • 3回目の再帰呼び出しでは、第1引数は厳密に減少し、他の引数はチェックされませんでした。

このように停止証明に失敗した場合、問題を発見するための良いテクニックは Lean.Parser.Command.declaration : commandtermination_by を使って予想される停止引数を明示的に示すことです。これにより、失敗したタクティクからのメッセージが表示されます。

配列のインデキシング

e₂ - e₁ の形式の式を測度と見なす目的は、ある上限までカウントするという一般的なイディオムをサポートすることであり、これは特に興味深い方法で配列を走査するような場合が該当します。ソート済みの配列に対して二分木探索を実行する以下の関数では、このヒューリスティックスは Lean が測度として j - i を見つけるのに役立ちます。

def binarySearch (x : Int) (xs : Array Int) : Option Nat := go 0 xs.size where go (i j : Nat) (hj : j xs.size := by omega) := if h : i < j then let mid := (i + j) / 2 let y := xs[mid] if x = y then some mid else if x < y then go i mid else go (mid + 1) j else none Try this: termination_by (j, j - i)termination_by?

この推測された停止引数が、最適または最小の測度ではなく、複数の任意の測度を使用しているという事実は、冗長な j を含む推測された測度を見ることでわかります:

Try this: termination_by (j, j - i)
推測中における停止証明のタクティク

Lean.Parser.Command.declaration : commanddecreasing_by で示されるタクティクは、停止 測度 を推論する際において、実際の停止証明のときとは少し違った使われ方をします。

  • 推論中において、このタクティクは Nat 上の < または を証明しようとする 単一の ゴールに適用されます。

  • 停止証明において、このタクティクは多くの同時ゴール(再帰呼び出しごとに1つ)に適用されます。これらのゴールはペアの辞書式順序を含む可能性があります。

その結果、ゴールに個別にアクセスし、明示的な停止引数で正常に動作するような Lean.Parser.Command.declaration : commanddecreasing_by ブロックが停止測度の推論に失敗することがあります:

Could not find a decreasing measure. The arguments relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) x1 x2 1) 911:16-23 ? ? 2) 912:27-40 _ _ 3) 912:20-41 _ _ Please use `termination_by` to specify a decreasing measure.def ack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => ack m 1 | m + 1, n + 1 => ack m (ack (m + 1) n) decreasing_by · apply Prod.Lex.left omega · apply Prod.Lex.right omega · apply Prod.Lex.left omega

明示的な Lean.Parser.Command.declaration : commanddecreasing_by 証明が与えられた場合は、常に Lean.Parser.Command.declaration : commandtermination_by 句を含めることをお勧めします。

強力すぎる推論

decreasing_tactic は辞書式順序に対して不完全であることによってバックトラックの必要性を回避するため、Lean はタクティクが証明できないゴールを導く停止 測度 を推論する場合があります。この場合、エラーメッセージは測度を見つけることができなかったことによるものではなく、タクティクが失敗したことによるものです。これは以下の notAck で見ることができます:

def notAck : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => notAck m 1 | m + 1, n + 1 => notAck m (notAck (m / 2 + 1) n) decreasing_by all_goals All goals completed! 🐙
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
case h
m n : Nat
⊢ m / 2 + 1 < m + 1

この場合、停止 測度 を明示することが有効です。

5.3.5. 相互整礎再帰(Mutual Well-Founded Recursion)🔗

Lean は 整礎再帰 を使った 相互再帰 関数の定義をサポートしています。相互再帰は 相互ブロック を使って導入することができますが、 Lean.Parser.Term.letrec : termlet rec 式と Lean.Parser.Command.declaration : commandwhere ブロックによっても得ることができます。相互整礎再帰の規則は エラボレーションのステップ の相互グループの結果である、実際に相互再帰的で持ち上げられた定義のグループに適用されます。

相互グループ内の関数が Lean.Parser.Command.declaration : commandtermination_by または Lean.Parser.Command.declaration : commanddecreasing_by 句を持つ場合、整礎再帰が試みられます。もし相互グループ内の 任意の 関数に対して Lean.Parser.Command.declaration : commandtermination_by を使用して停止 測度 を指定した場合、グループ内の 全ての 関数は停止測度を指定する必要があり、それらは同じ型でなければなりません。

停止引数が指定されていない場合、停止引数は 上述したように推論されます 。相互再帰の場合、推論において基本測度の低級な、すなわち、相互グループ内の各関数に対してその関数に対しては 1、他の関数に対して 0 である測度が考慮されます。これによって Lean はパラメータが減少しなくても、ある関数から別の関数への呼び出しが許可されるように関数を並べることができます。

パラメータが減少しない相互再帰

以下の相互関数定義では、 g から f への呼び出しにおいてパラメータは減少しません。それにもかかわらず、この定義は、追加の基本測度によって関数自体に課される順序によって許容されます。

mutual def f : (n : Nat) Nat | 0 => 0 | n+1 => g n Try this: termination_by n => (n, 0)termination_by? def g (n : Nat) : Nat := (f n) + 1 Try this: termination_by (n, 1)termination_by? end

f の推測された停止引数は以下です:

Try this: termination_by n => (n, 0)

g の推測された停止引数は以下です:

Try this: termination_by (n, 1)

5.3.6. 理論と構成(Theory and Construction)

本節では、 整礎再帰 による停止証明の根底にある数学的構造をごく簡単に紹介します。整礎再帰で定義された関数のエラボレーションは WellFounded.fix 演算子に基づいています。

🔗def
WellFounded.fix.{u, v} {α : Sort u}
    {C : αSort v} {r : ααProp}
    (hwf : WellFounded r)
    (F : (x : α) →
      ((y : α) → r y xC y) → C x)
    (x : α) : C x

α は関数の(変換する)パラメータによってインスタンス化され、 PSigma を用いて1つの型にまとめられます。 WellFounded 関係は invImage を介して停止 測度 から構築されます。

🔗def
invImage.{u_1, u_2} {α : Sort u_1}
    {β : Sort u_2} (f : αβ)
    (h : WellFoundedRelation β)
    : WellFoundedRelation α

関数の本体は WellFounded.fix に渡され、パラメータは適切にパックおよびアンパックされ、再帰呼び出しは WellFounded.fix が提供する値への呼び出しに置き換えられます。 Lean.Parser.Command.declaration : commanddecreasing_by タクティクによって生成された停止証明は適切な位置に挿入されます。

最後に、 WellFounded.fix_eq から再帰関数の等式定理と展開の定理が証明されます。これらの定理は引数のパッキングとアンパッキングの詳細を隠蔽し、もとの定義の観点から関数の動作を記述します。

相互再帰の場合、 PSum を使って関数の引数を組み合わせ、その直和型をパターンマッチすることで同等の非相互関数が構築されます。

WellFounded の定義は、その関係の アクセス可能な要素 (accessible element)という概念に基づいています。

🔗inductive predicate
WellFounded.{u} {α : Sort u} (r : ααProp)
    : Prop

A relation r is WellFounded if all elements of α are accessible within r. If a relation is WellFounded, it does not allow for an infinite descent along the relation.

If the arguments of the recursive calls in a function definition decrease according to a well founded relation, then the function terminates. Well-founded relations are sometimes called Artinian or said to satisfy the “descending chain condition”.

Constructors

intro.{u} {α : Sort u} {r : ααProp}
    (h : ∀ (a : α), Acc r a) : WellFounded r
🔗inductive predicate
Acc.{u} {α : Sort u} (r : ααProp)
    : αProp

Acc is the accessibility predicate. Given some relation r (e.g. <) and a value x, Acc r x means that x is accessible through r:

x is accessible if there exists no infinite sequence ... < y₂ < y₁ < y₀ < x.

Constructors

intro.{u} {α : Sort u} {r : ααProp} (x : α)
    (h : ∀ (y : α), r y xAcc r y) : Acc r x

A value is accessible if for all y such that r y x, y is also accessible. Note that if there exists no y such that r y x, then x is accessible. Such an x is called a base case.

引き算の繰り返しによる割り算:停止証明

引き算の繰り返しによる割り算の定義は、整礎再帰を使って明示的に描くことができます。

noncomputable def div (n k : Nat) : Nat := (inferInstanceAs (WellFoundedRelation Nat)).wf.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + (r (n - k) <| α:Type un✝:Natk:Natn:Natr:(y : Nat) → WellFoundedRelation.rel y nNath✝:¬k = 0h:¬k > nWellFoundedRelation.rel (n - k) n α:Type un✝:Natk:Natn:Natr:(y : Nat) → WellFoundedRelation.rel y nNath✝:¬k = 0h:¬k > nn - k < n All goals completed! 🐙)) n

整礎再帰はコンパイラにサポートされていないため、この定義は Lean.Parser.Command.declaration : commandnoncomputable をマークしなければなりません。 再帰子 と同様に、これは Lean の論理の一部です。

割り算の定義は以下の式を満たす必要があります:

  • {n k : Nat}, (k = 0) div n k = 0

  • {n k : Nat}, (k > n) div n k = 0

  • {n k : Nat}, (k 0) (¬ k > n) div n k = div (n - k) k

この簡約の挙動は definitionally に保持されません:

theorem div.eq0 : div n 0 = 0 := n:Natdiv n 0 = 0 n:Natdiv n 0 = 0
tactic 'rfl' failed, the left-hand side
  div n 0
is not definitionally equal to the right-hand side
  0
n : Nat
⊢ div n 0 = 0

しかし、 WellFounded.fix_eq を使って整礎再帰を展開すれば、3つの等式が成り立つことが証明できます:

theorem div.eq0 : div n 0 = 0 := n:Natdiv n 0 = 0 n:Natproof_1.fix (fun n r => if h : 0 = 0 then 0 else if h : 0 > n then 0 else 1 + r (n - 0) ⋯) n = 0 All goals completed! 🐙 theorem div.eq1 : k > n div n k = 0 := k:Natn:Natk > ndiv n k = 0 k:Natn:Nath:k > ndiv n k = 0 k:Natn:Nath:k > nproof_1.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) n = 0 k:Natn:Nath:k > n(if h : k = 0 then 0 else if h : k > n then 0 else 1 + (fun y x => proof_1.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) y) (n - k) ⋯) = 0 k:Natn:Nath:k > n¬k = 0 → kn → 1 + proof_1.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ⋯) (n - k) = 0 k:Natn:Nath:k > na✝¹:¬k = 0a✝:kn1 + proof_1.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ⋯) (n - k) = 0; All goals completed! 🐙 theorem div.eq2 : ¬ k = 0 ¬ (k > n) div n k = 1 + div (n - k) k := k:Natn:Nat¬k = 0 → ¬k > ndiv n k = 1 + div (n - k) k k:Natn:Nata✝¹:¬k = 0a✝:¬k > ndiv n k = 1 + div (n - k) k k:Natn:Nata✝¹:¬k = 0a✝:¬k > nproof_1.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) n = 1 + proof_1.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) (n - k) k:Natn:Nata✝¹:¬k = 0a✝:¬k > n(if h : k = 0 then 0 else if h : k > n then 0 else 1 + (fun y x => proof_1.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) y) (n - k) ⋯) = 1 + proof_1.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) (n - k) k:Natn:Nata✝¹:¬k = 0a✝:knn < k → 0 = 1 + proof_1.fix (fun n r => if h : n < k then 0 else 1 + r (n - k) ⋯) (n - k) All goals completed! 🐙