5.2. 構造的再帰(Structural Recursion)🔗

構造的再帰関数とは、各再帰呼び出しが引数よりも構造的に小さい項に対して行われる関数のことです。同じパラメータはすべての再帰呼び出しで減少しなければなりません;このパラメータは 減少パラメータ (decreasing parameter)と呼ばれます。構造的再帰は、再帰子が提供する原始再帰よりも強力です。なぜなら、再帰呼び出しが引数の直前の部分項だけでなく、任意の部分項を使用できるからです。しかし、構造的再帰を実装するために使用される構成は再帰子を使用して実装されます;これらの補助構成は 帰納型の節 で説明されています。

構造的再帰を支配する規則は性質からして 構文的 なものです。再帰的定義の中には構造的再帰である計算動作を示すものの、これらの規則では受け入れられないものが数多く存在します;これは解析が完全に自動化されていることの基本的な結果です。 整礎再帰 は再帰関数が構造的再帰ではない状況でも停止性を示すために使用できる意味論的アプローチを提供しますが、構造的再帰に従って計算される関数が構文的要件を満たさない場合にも使用できます。

構造的再帰と引き算

以下の関数 countdown は構造的再帰です。パターンマッチで2番目の分岐において、パラメータ n はパターン n' + 1 に対してマッチしますが、これは n'n の直接の部分項であることを意味します:

def countdown (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown n'

パターンマッチを同等な真偽値のテストと引き算に置き換えるとエラーになります:

def fail to show termination for countdown' with errors failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' 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 n : Nat h✝ : ¬(n == 0) = true n' : Nat := n - 1 ⊢ n - 1 < ncountdown' (n : Nat) : List Nat := if n == 0 then [] else let n' := n - 1 n' :: countdown' n'
fail to show termination for
  countdown'
with errors
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    countdown' 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
n : Nat
h✝ : ¬(n == 0) = true
n' : Nat := n - 1
⊢ n - 1 < n

これはパラメータ n に対するパターンマッチが無いためです。この関数は確実に停止しますが、それをもたらす引数の性質として Nat帰納型 であるという一般的な特徴ではなく、if・等号のテスト・引き算などの特徴に基づいています。これらの引数は 整礎再帰 を使って表現され、関数の定義に少し変更を加えることで、Lean の整礎再帰の自動サポートが代替的な終了証明を構築できるようになります。このバージョンは、真偽値の等号テストではなく、 Nat についての propositional equality の決定可能性に対して分岐します:

def countdown' (n : Nat) : List Nat := if n = 0 then [] else let n' := n - 1 n' :: countdown' n'

ここでは、Lean の自動化は propositional equality と引き算に関する事実から停止性の証明を自動的に構築します。その裏では、構造的再帰ではなく整礎再帰を使っています。

構造的再帰は明示的にも自動的にも使用することができます。明示的な構造的再帰では、関数定義で 減少パラメータ がどのパラメータであるかを宣言します。停止に関する戦略が明示的に宣言されていない場合、Lean は 整礎再帰 で使用する減少する測度と同様に、減少パラメータの検索を実行します。構造的再帰を明示的に注釈すると、次のような利点があります:

  • 検索が発生しないため、エラボレーションの速度が上がります。

  • コードを読む人のために停止引数が文書化されます。

  • 構造的再帰が明示的に望まれている状況では、整礎再帰が誤って使用されることを防ぐことができます。

5.2.1. 明示的な構造的再帰(Explicit Structural Recursion)

構造的再帰を明示的に使用するには、関数または定理の定義に 減少パラメータ を指定した Lean.Parser.Command.declaration : commandtermination_by structural 句を注釈します。減少パラメータはシグネチャで指定されたパラメータを参照することができます。シグネチャが関数型となっている場合、減少パラメータはシグネチャ内の名前のないパラメータとなる場合があります;この場合、残りのパラメータの名前を矢印( Lean.Parser.Command.declaration : command=> )の前に書いて導入することができます。

減少パラメータの指定

減少パラメータが関数の名前付きパラメータである場合、その名前を参照して指定することができます。

def half (n : Nat) : Nat := match n with | 0 | 1 => 0 | n + 2 => half n + 1 termination_by structural n

減少パラメータの名前がシグネチャに無い場合、 Lean.Parser.Command.declaration : commandtermination_by 句でローカルに名前を導入することができます。

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1 termination_by structural n => n
syntaxExplicit Structural Recursion

termination_by structural 句は減少パラメータを導入します。

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 structural (ident* =>)? term

オプショナルな => の前の識別子は、その関数の宣言ヘッダでまだ束縛されていない関数パラメータをスコープに入れることができ、必須の項はヘッダやローカルで導入された関数のパラメータのうち1つを指定しなければなりません。

減少パラメータは以下の条件を満たさなければなりません:

  • 型が 帰納型 でなければなりません。

  • 減少パラメータの帰納型または添字付けられた型の族がデータ型のパラメータを持つ場合、これらのデータ型パラメータは、それ自身が fixed 接頭辞 の一部である関数パラメータにのみ依存することができます。

fixed パラメータ (fixed parameter)はすべての再帰呼び出しで変更されずに渡される関数パラメータであり、再帰パラメータの型の添字ではありません。 fixed 接頭辞 (fixed prefix)は、関数パラメータの中で、すべてが fixed である最長の接頭辞です。

不適格な減少パラメータ

減少パラメータは帰納型でなければなりません。 notInductive では、減少パラメータとして関数が指定されています:

def notInductive (x : Nat Nat) : Nat := notInductive (fun n => x (n+1)) cannot use specified parameter for structural recursion: its type is not an inductivetermination_by structural x
cannot use specified parameter for structural recursion:
  its type is not an inductive

減少パラメータが添字付けられた型の族の場合、添字はすべて変数でなければなりません。 constantIndex では、代わりに添字付けられた型の族 Fin' が定数に適用されます:

inductive Fin' : Nat Type where | zero : Fin' (n+1) | succ : Fin' n Fin' (n+1) def constantIndex (x : Fin' 100) : Nat := constantIndex .zero cannot use specified parameter for structural recursion: its type Fin' is an inductive family and indices are not variables Fin' 100termination_by structural x
cannot use specified parameter for structural recursion:
  its type Fin' is an inductive family and indices are not variables
    Fin' 100

減少パラメータの型のパラメータは、変化するパラメータや添字の後に来る関数パラメータに依存してはいけません。 afterVarying では、 fixed 接頭辞 は空です。なぜなら、最初のパラメータ n は変化するため、p は fixed 接頭辞の一部ではないからです:

inductive WithParam' (p : Nat) : Nat Type where | zero : WithParam' p (n+1) | succ : WithParam' p n WithParam' p (n+1) def afterVarying (n : Nat) (p : Nat) (x : WithParam' p n) : Nat := afterVarying (n+1) p .zero cannot use specified parameter for structural recursion: its type is an inductive datatype WithParam' p n and the datatype parameter p depends on the function parameter p which does not come before the varying parameters and before the indices of the recursion parameter.termination_by structural x
cannot use specified parameter for structural recursion:
  its type is an inductive datatype
    WithParam' p n
  and the datatype parameter
    p
  depends on the function parameter
    p
  which does not come before the varying parameters and before the indices of the recursion parameter.

さらに、すべての再帰的な関数呼び出しは減少パラメータの 厳密な部分項 (strict sub-term)に対して行われなければなりません。

  • 減少パラメータはそれ自体が部分項ですが、厳密な部分項ではありません。

  • 部分項が 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 式やその他のパターンマッチ構文の 判別子 の場合、判別子にマッチするパターンは各 マッチ選択肢右辺 の部分項です。特に マッチの一般化 の規則は、判別子を右辺におけるパターン項の出現に接続するために使用されます;つまり、 definitional equality を考慮します。判別子が厳密な部分項である場合に限り、パターンは 厳密な 部分項となります。

  • 部分項が引数に適用されるコンストラクタである場合、その再帰的引数は厳密な部分項です。

入れ子のパターンと部分項

以下の例では、減少パラメータ n は入れ子のパターン .succ (.succ n) にマッチします。したがって、 .succ (.succ n)n の(非厳密な)部分項であり、その結果 n.succ n はともに厳密な部分項であるため、この定義は受け入れられます。

def fib : Nat Nat | 0 | 1 => 1 | .succ (.succ n) => fib n + fib (.succ n) termination_by structural n => n

このことをわかりやすくするために、この例では Nat 固有の書き方である n+1n+2 の代わりに .succ n.succ (.succ n) を使用しています。

複雑な式へのマッチによるエラボレーションの阻害

以下の例では、減少パラメータ nLean.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 式の直接の 判別子 ではありません。したがって、 n'n の部分項とはみなされません。

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application half n' def half (n : Nat) : Nat := match Option.some n with | .some (n' + 2) => half n' + 1 | _ => 0 termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    half n'

整礎再帰 を使うことで、判別子をマッチのパターンに明示的に結び付けることで、この定義は許容されます。

def half (n : Nat) : Nat := match h : Option.some n with | .some (n' + 2) => half n' + 1 | _ => 0 termination_by n decreasing_by n:Natn':Nath:n = n' + 1 + 1n' < n' + 1 + 1; All goals completed! 🐙

同様に、以下の例は失敗します: xs.tailxs の厳密な部分項に簡約されるにもかかわらず、上記の規則によりこの事実を Lean が見ることができません。具体的には、 xs.tailxs の厳密な部分項に対して definitionally equal ではありません。

failed to infer structural recursion: Cannot use parameter #2: failed to eliminate recursive application listLen xs.tail def listLen : List α Nat | [] => 0 | xs => listLen xs.tail + 1 termination_by structural xs => xs
構造的再帰に対する同時マッチとマッチペア

停止性を証明するために使用される戦略の重要な帰結は、 2つの 判別子 の同時マッチはペアのマッチと同等ではない ということです。同時マッチは判別子とパターンの間の繋がりを維持し、その 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 における期待される型と同様に、ローカルコンテキストにおける仮定の型を絞り込むことを可能にします。基本的に、 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 に対するエラボレーションの規則では判別子を特別に扱っており、プログラムの実行時の意味を保持する方法で判別子を変更してもコンパイル時の意味が保持されるとは限りません。

2つの自然数の最小値を求めるこの関数は、最初のパラメータに対する構造的再帰によって定義されます:

def min' (n k : Nat) : Nat := match n, k with | 0, _ => 0 | _, 0 => 0 | n' + 1, k' + 1 => min' n' k' + 1 termination_by structural n

両方のパラメータに対する同時マッチをペアのマッチに置き換えると停止分析が失敗します:

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application min' n' k' def min' (n k : Nat) : Nat := match (n, k) with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' n' k' + 1 termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    min' n' k'

これは、狭義により小さな引数の値への再帰呼び出しをマッチさせる際に、分析がパラメータの直接的なパターンマッチしか考慮しないためです。判別子をペアで包むと、そのつながりが断ち切られます。

ペア内の値に対する構造的再帰

ペアの2つの成分の最小値を求めるこの関数は、構造的再帰によってエラボレートすることができません。

failed to infer structural recursion: Cannot use parameter nk: the type Nat × Nat does not have a `.brecOn` recursor def min' (nk : Nat × Nat) : Nat := match nk with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' (n', k') + 1 termination_by structural nk
failed to infer structural recursion:
Cannot use parameter nk:
  the type Nat × Nat does not have a `.brecOn` recursor

これは、パラメータの型 Prod が再帰的ではないからです。したがって、そのコンストラクタにはパターンマッチで公開できる再帰的なパラメータはありません。

しかし、この定義は 整礎再帰 を使うことで受け入れられます:

def 'min'' has already been declaredmin' (nk : Nat × Nat) : Nat := match nk with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' (n', k') + 1 termination_by nk
構造的再帰と definitional equality

countdown の再帰的な出現が減少パラメータの厳密な部分項ではない項に適用されているにもかかわらず、以下の定義は許容されます:

def countdown (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown (n' + 0) termination_by structural n

これは n' + 0 が、 n の厳密な部分項である n'definitionally equal であるからです。パターンマッチで得られる 部分項 は、definitional equality を考慮している マッチの一般化 の規則を用いて 判別子 に接続されます。

countdown' において、その再帰的な出現は 0 + n' に適用されますが、自然数の足し算は第2引数に対して構造的再帰であるため n' に definitionally equal ではありません:

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' (0 + n') def countdown' (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown' (0 + n') termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    countdown' (0 + n')

5.2.2. 相互構造的再帰(Mutual Structural Recursion)🔗

Lean は構造的再帰を利用した mutually recursive 関数の定義をサポートしています。相互再帰は mutual block を使って導入できますが、 Lean.Parser.Term.letrec : termlet rec 式と Lean.Parser.Command.declaration : commandwhere ブロックにおいても得ることができます。相互再帰の規則は、相互グループの エラボレーションのステップ から得られる、実際に相互再帰的で持ち上げられた定義のグループに適用されます。相互グループ内のすべての関数が、その関数の減少引数を示す termination_by structural 注釈を持っている場合、構造的再帰は定義の翻訳に使用されます。

減少引数に対する要件は上述のものから拡張されます:

  • すべての減少引数の型は、同じ帰納型、またはより一般的には同じ 帰納型の相互グループ からのものでなければなりません。

  • 減少パラメータの型におけるパラメータは、すべての関数で同じでなければならず、関数引数の 共通の fixed 接頭辞にのみ依存することができます・

関数は相互帰納型と一対一に対応している必要はありません。複数の関数が同じ型である減少引数を持つことができ、減少引数を持つ相互再帰なすべての型が対応する関数を持つ必要はありません。

非相互型に対する相互構造的再帰

以下の例は非相互帰納データ型に対する相互再帰を示しています:

mutual def even : Nat Prop | 0 => True | n+1 => odd n termination_by structural n => n def odd : Nat Prop | 0 => False | n+1 => even n termination_by structural n => n end
相互型に対する相互構造的再帰

以下の例は相互帰納型に対する相互再帰を示しています。関数 Exp.sizeApp.size は相互再帰的です。

mutual inductive Exp where | var : String Exp | app : App Exp inductive App where | fn : String App | app : App Exp App end mutual def Exp.size : Exp Nat | .var _ => 1 | .app a => a.size termination_by structural e => e def App.size : App Nat | .fn _ => 1 | .app a e => a.size + e.size + 1 termination_by structural a => a end

App.numArgs の定義は、 App 型に対して構造的再帰です。これは相互グループ内のすべての帰納型を処理する必要がないことを示しています。

def App.numArgs : App Nat | .fn _ => 0 | .app a _ => a.numArgs + 1 termination_by structural a => a
Planned Content

Describe mutual structural recursion over nested inductive types.

Tracked at issue #235

5.2.3. 構造的再帰の推論(Inferring Structural Recursion)🔗

termination_by 句が再帰または相互再帰関数定義に存在しない場合、Lean はすべての該当するパラメータに対して順番に効率よく、構造的に減少する適切な引数の推論を試みます。この探索が失敗した場合、Lean は 整礎再帰 の推論を試みます。

相互再帰関数に対しては、組み合わせ爆発を避けるために、限界まですべてのパラメータの組み合わせが試行されます。一部の相互再帰関数にのみ termination_by structural 句がある場合、それらのパラメータのみが考慮され、それ以外の関数ではすべてのパラメータが構造的再帰の候補となります。

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

推論された停止注釈

Lean は関数 half が構造的再帰であることを自動的に推論します。 termination_by? 句を使用すると、推論された停止注釈が表示され、ワンクリックでソースファイルに自動的に追加されます。

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1 Try this: termination_by structural x => xtermination_by?
Try this: termination_by structural x => x

5.2.4. 累積再帰を使用したエラボレーション(Elaboration Using Course-of-Values Recursion)

本節では、構造的再帰関数をさらに詳しく説明します。このエラボレーションは帰納型の再帰子から自動生成される belowbrecOn 構成 を使用します。

再帰 vs 再帰子

自然数の足し算は、第2引数に対する再帰によって定義することができます。この関数は単純な構造的再帰です。

def add (n : Nat) : Nat Nat | .zero => n | .succ k => .succ (add n k)

Nat.rec を使って定義すると、多くの人が慣れ親しんでいる表記からかけ離れています。

def add' (n : Nat) := Nat.rec (motive := fun _ => Nat) n (fun k soFar => .succ soFar)

関数パラメータの直接の子要素ではないデータに対して行われる構造的再帰呼び出しには、創造性か、複雑ですが体系的なエンコーディングのどちらかが必要です。

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1

この関数についての1つの考え方は、呼び出すたびにビットを反転させ、ビットがセットされたときだけ結果をインクリメントする構造的再帰です。

def helper : Nat Bool Nat := Nat.rec (motive := fun _ => Bool Nat) (fun _ => 0) (fun _ soFar => fun b => (if b then Nat.succ else id) (soFar !b)) def half' (n : Nat) : Nat := helper n false [0, 0, 1, 1, 2, 2, 3, 3, 4]#eval [0, 1, 2, 3, 4, 5, 6, 7, 8].map half'
[0, 0, 1, 1, 2, 2, 3, 3, 4]

創造性の代わりに、 累積再帰 (course-of-values recursion)と呼ばれる一般的なテクニックを使用することができます。累積再帰では、再帰子で定義されたすべての帰納型に対して体系的に導出できる補助関数を使用します;Lean はこれらを自動で導出します。すべての Nat n に対して、 n.below (motive := mot) 型はすべての k < n に対して mot k 型の値を提供し、繰り返された依存ペア型として表現されます。累積再帰子 Nat.brecOn は関数が任意の小さい Nat に対して結果を使用することを可能にします。これを使用して関数を定義することは不便です:

noncomputable def half'' (n : Nat) : Nat := Nat.brecOn n (motive := fun _ => Nat) fun k soFar => match k, soFar with | 0, _ | 1, _ => 0 | _ + 2, _, h, _ => h + 1

この関数は Lean.Parser.Command.declaration : commandnoncomputable とマークされていますが、これはコンパイラが累積再帰のコード生成をサポートしていないためです。というのもこれは効率的なコードではなく推論用を意図されたものだからです。しかし、カーネルを関数のテストのために使用することができます:

[0, 0, 1, 1, 2, 2, 3, 3, 4]#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]

必要に応じて half'' の本体で依存パターンマッチを再帰子(具体的には Nat.casesOn )を使ってエンコードすることもできます:

noncomputable def half''' (n : Nat) : Nat := n.brecOn (motive := fun _ => Nat) fun k => k.casesOn (motive := fun k' => (k'.below (motive := fun _ => Nat)) Nat) (fun _ => 0) (fun k' => k'.casesOn (motive := fun k'' => (k''.succ.below (motive := fun _ => Nat)) Nat) (fun _ => 0) (fun _ soFar => soFar.2.1.succ))

この定義も動作します。

[0, 0, 1, 1, 2, 2, 3, 3, 4]#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]

しかし、もはや本来の定義からかけ離れ、多くの人にとって理解しにくいものになっています。再帰子は優れた論理的基礎ですが、プログラムや証明を書くには簡単な方法ではありません。

構造的再帰分析は再帰的な事前定義を適切な構造的再帰の構成の利用に変換しようと試みます。このステップでは、パターンマッチはすでにマッチ関数の使用に変換されています;これらの関数は停止チェッカによって特別に扱われます。次に、各パラメータのグループに対して brecOn を使った変換が試みられます。

Course-of-Values Tables

以下の定義は List.below と同等です:

def List.below' {α : Type u} {motive : List α Sort u} : List α Sort (max 1 u) | [] => PUnit | _ :: xs => motive xs ×' xs.below' (motive := motive)

言い換えると、与えられた 動機 に対して、 List.below' はリストのすべての接尾辞に対する動機の実現を含む型です。

より再帰的な引数は直積型の反復をさらに入れ子にする必要があります。例えば、二分木には2つの再帰が出現します。

inductive Tree (α : Type u) : Type u where | leaf | branch (left : Tree α) (val : α) (right : Tree α)

対応する累積テーブルにはすべてのサブツリーに対する動機の実現が含まれています:

def Tree.below' {α : Type u} {motive : Tree α Sort u} : Tree α Sort (max 1 u) | .leaf => PUnit | .branch left _val right => motive left ×' motive right ×' left.below' (motive := motive) ×' right.below' (motive := motive)

リストと木のどちらについても、brecOn 演算子はコンストラクタごとに1つではなく、1つのケースだけを想定しています。このエースはリストまたは木を、すべての小さい値に対する結果のテーブルと一緒に受け入れます;これにより、このケースは与えられた値に対する動機を満たすべきです。提供された値の依存ケース分析によって、メモテーブルの型が自動的に絞り込まれ、必要なものがすべて提供されます。

以下の定義はそれぞれ List.brecOnTree.brecOn に同等です。プリミティブな再帰補助関数 List.brecOnTableTree.brecOnTable は最終結果と共に累積テーブルを計算し、実際の brecOn 演算子の定義は単に結果を射影します。

def List.brecOnTable {α : Type u} {motive : List α Sort u} (xs : List α) (step : (ys : List α) ys.below' (motive := motive) motive ys) : motive xs ×' xs.below' (motive := motive) := match xs with | [] => step [] PUnit.unit, PUnit.unit | x :: xs => let res := xs.brecOnTable (motive := motive) step let val := step (x :: xs) res val, res def Tree.brecOnTable {α : Type u} {motive : Tree α Sort u} (t : Tree α) (step : (ys : Tree α) ys.below' (motive := motive) motive ys) : motive t ×' t.below' (motive := motive) := match t with | .leaf => step .leaf PUnit.unit, PUnit.unit | .branch left val right => let resLeft := left.brecOnTable (motive := motive) step let resRight := right.brecOnTable (motive := motive) step let branchRes := resLeft.1, resRight.1, resLeft.2, resRight.2 let val := step (.branch left val right) branchRes val, branchRes def List.brecOn' {α : Type u} {motive : List α Sort u} (xs : List α) (step : (ys : List α) ys.below' (motive := motive) motive ys) : motive xs := (xs.brecOnTable (motive := motive) step).1 def Tree.brecOn' {α : Type u} {motive : Tree α Sort u} (t : Tree α) (step : (ys : Tree α) ys.below' (motive := motive) motive ys) : motive t := (t.brecOnTable (motive := motive) step).1

below 構成は型の各値から すべての 小さい値に対する関数呼び出しの結果へのマッピングです;これはすべての小さい値の結果をすでの格納したメモ化テーブルとして理解することができます。below 構成において表現される「小さい値」という概念は 厳密な部分項 の定義に直接対応します。

再帰子は帰納型のコンストラクタの各引数を期待します;これらの引数は ι簡約 の間にコンストラクタの引数(および再帰パラメータに対する再帰の結果)とともに呼び出されます。一方、累積再帰の演算子 brecOn は一度にすべてのコンストラクタをカバーする単一のケースだけを期待します。このケースには値と、与えられた値よりも小さいすべての値に対する再帰の結果を含む below テーブルが提供されます;これは与えられた値に対する動機を満たすテーブルの内容を使用するべきです。関数が与えられたパラメータ(もしくはパラメータグループ)に対して構造的再帰である場合、すべての再帰呼び出しの結果はすでにこのテーブルに存在することになります。

再帰関数の本体が関数のパラメータの1つに対する brecOn の呼び出しに変換されると、そのパラメータと値の累積テーブルがスコープに入ります。この解析は関数本体を走査し、再帰的な呼び出しを探します。パラメータがマッチした場合、ローカルコンテキストに出現するパラメータは 一般化 され、そのパターンでインスタンス化されます;これは累積テーブルの型についても真です。通常、このパターンマッチの結果、累積テーブルの型はより具体的になり、より小さな値の再帰結果にアクセスできるようになります。この一般化処理は、パターンがマッチ識別子の 部分項 であるという規則を実装します。関数の再帰的な出現が検出されると、累積テーブルが参照され、チェックされている引数の結果が含まれているかどうかが確認されます。もし含まれていれば、再帰呼び出しはテーブルからの射影で置き換えることができます。含まれていない場合は、当該パラメータは構造的再帰をサポートしていません。

エラボレーションの一連の流れ

half のエラボレーションを進める最初のステップは、手作業でより単純な形に脱糖することです。これは Lean の動作方法とは一致しませんが、 OfNat インスタンスの数が少なければその出力はずっと読みやすくなります。この読みやすい定義:

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1

は、以下のやや低レベルのバージョンに書き換えることができます:

def half : Nat Nat | .zero | .succ .zero => .zero | .succ (.succ n) => half n |>.succ

エラボレータは、再帰はまだ存在するものの、それ以外は Lean のコア型理論に沿った事前定義をエラボレートすることから始めます。コンパイラの事前定義の追跡をオンにすると、プリティプリンタがより明示的になり、その結果事前定義が見えるようになります:

set_option trace.Elab.definition.body true in set_option pp.all true in def half : Nat Nat | .zero | .succ .zero => .zero | .succ (.succ n) => half n |>.succ

返される追跡メッセージは以下です:

[Elab.definition.body] half : Nat → Nat :=
    fun (x : Nat) =>
      half.match_1.{1} (fun (x : Nat) => Nat) x
        (fun (_ : Unit) => Nat.zero)
        (fun (_ : Unit) => Nat.zero)
        fun (n : Nat) => Nat.succ (half n)

この補助マッチ関数の定義は以下です:

def half.match_1.{u_1} : (motive : Nat → Sort u_1) → (x : Nat) → (Unit → motive Nat.zero) → (Unit → motive 1) → ((n : Nat) → motive n.succ.succ) → motive x := fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n#print half.match_1
def half.match_1.{u_1} : (motive : Nat → Sort u_1) →
  (x : Nat) → (Unit → motive Nat.zero) → (Unit → motive 1) → ((n : Nat) → motive n.succ.succ) → motive x :=
fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n

より読みやすくフォーマットすると、この定義は以下のようになります:

def half.match_1'.{u} : (motive : Nat Sort u) (x : Nat) (Unit motive Nat.zero) (Unit motive 1) ((n : Nat) motive n.succ.succ) motive x := fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n

つまり、 half で使用されるパターンの特定の構成は half.match_1 に取り込まれます。

以下の定義は half の事前定義をより読みやすくしたものです:

def half' : Nat Nat := fun (x : Nat) => half.match_1 (motive := fun _ => Nat) x (fun _ => 0) -- Case for 0 (fun _ => 0) -- Case for 1 (fun n => Nat.succ (half' n)) -- Case for n + 2

この関数を構造的再帰関数として定義するには、まず bRecOn の呼び出しを確立します。この定義は Lean.Parser.Command.declaration : commandnoncomputable とマークしなければなりません。なぜなら Lean は Nat.brecOn のような再帰子のためのコード生成をサポートしていないからです。

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => don't know how to synthesize placeholder context: x n : Nat table : Nat.below n ⊢ Nat_ /- To translate: half.match_1 (motive := fun _ => Nat) x (fun _ => 0) -- Case for 0 (fun _ => 0) -- Case for 1 (fun n => Nat.succ (half' n)) -- Case for n + 2 -/

次のステップは、もとの関数本体にある xbrecOn が提供する n で置き換えることです。table の型は x に依存するため、 half.match_1 でケースを分割する際にも一般化する必要があり、追加のパラメータを持つ同期となります。

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n don't know how to synthesize placeholder for argument 'h_1' context: x n : Nat table : Nat.below n ⊢ Unit → (fun k => Nat.below k → Nat) Nat.zero_ don't know how to synthesize placeholder for argument 'h_2' context: x n : Nat table : Nat.below n ⊢ Unit → (fun k => Nat.below k → Nat) 1_ don't know how to synthesize placeholder for argument 'h_3' context: x n : Nat table : Nat.below n ⊢ (n : Nat) → (fun k => Nat.below k → Nat) n.succ.succ_) table /- To translate: (fun _ => 0) -- Case for 0 (fun _ => 0) -- Case for 1 (fun n => Nat.succ (half' n)) -- Case for n + 2 -/

3つのケースのプレースホルダは以下の型を期待します:

don't know how to synthesize placeholder for argument 'h_1'
context:
x n : Nat
table : Nat.below n
⊢ Unit → (fun k => Nat.below k → Nat) Nat.zero
don't know how to synthesize placeholder for argument 'h_2'
context:
x n : Nat
table : Nat.below n
⊢ Unit → (fun k => Nat.below k → Nat) 1
don't know how to synthesize placeholder for argument 'h_3'
context:
x n : Nat
table : Nat.below n
⊢ (n : Nat) → (fun k => Nat.below k → Nat) n.succ.succ

事前定義にある最初の2つのケースは定数関数で、チェックする再帰はありません:

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n (fun () _ => .zero) (fun () _ => .zero) don't know how to synthesize placeholder for argument 'h_3' context: x n : Nat table : Nat.below n ⊢ (n : Nat) → (fun k => Nat.below k → Nat) n.succ.succ_) table /- To translate: (fun n => Nat.succ (half' n)) -- Case for n + 2 -/

最後のケースには再帰呼び出しが含まれています。これは累積テーブルへの検索に変換されるべきです。最後のホールの型をより読みやすく表現すると次のようになります:

(n : Nat) Nat.below (motive := fun _ => Nat) n.succ.succ Nat

これは以下と同等です:

(n : Nat) Nat ×' (Nat ×' Nat.below (motive := fun _ => Nat) n) Nat

累積テーブルの最初の Natn + 1 に対する再帰の結果であり、2番目は n に対する再帰の結果です。したがって、再帰呼び出しは検索に置き換えることができ、エラボレーションが成功します:

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n (fun () _ => .zero) (fun () _ => .zero) (fun _ table => Nat.succ table.2.1) table unexpected end of input; expected ')', ',' or ':'

実際のエラボレータは新しい名前を持つ番兵型を動機に挿入することによって、構造的再帰のためにチェックされるパラメータと累積テーブル内での位置との間の関係を追跡します。

Planned Content

A description of the elaboration of mutually recursive functions

Tracked at issue #56