4.4. Recursive Definitions
Allowing arbitrary recursive function definitions would make Lean's logic inconsistent.
General recursion makes it possible to write circular proofs: "proposition P
is true because proposition P
is true".
Outside of proofs, an infinite loop could be assigned the type Empty
, which can be used with Lean.Parser.Term.nomatch : term
Empty match/ex falso. `nomatch e` is of arbitrary type `α : Sort u` if
Lean can show that an empty set of patterns is exhaustive given `e`'s type,
e.g. because it has no constructors.
nomatch
or Empty.rec
to prove any theorem.
Banning recursive function definitions outright would render Lean far less useful: inductive types are key to defining both predicates and data, and they have a recursive structure. Furthermore, most useful recursive functions do not threaten soundness, and infinite loops usually indicate mistakes in definitions rather than intentional behavior. Instead of banning recursive functions, Lean requires that each recursive function is defined safely. While elaborating recursive definitions, the Lean elaborator also produces a justification that the function being defined is safe.The section on the elaborator's output in the overview of elaboration contextualizes the elaboration of recursive definitions in the overall context of the elaborator.
There are four main kinds of recursive functions that can be defined:
- Structurally recursive functions
Structurally recursive functions take an argument such that the function makes recursive calls only on strict sub-components of said argument.Strictly speaking, arguments whose types are indexed families are grouped together with their indices, with the whole collection considered as a unit. The elaborator translates the recursion into uses of the argument's recursor. Because every type-correct use of a recursor is guaranteed to avoid infinite regress, this translation is evidence that the function terminates. Applications of functions defined via recursors are definitionally equal to the result of the recursion, and are typically relatively efficient inside the kernel.
- Recursion over well-founded relations
Many functions are also difficult to convert to structural recursion; for instance, a function may terminate because the difference between an array index and the size of the array decreases as the index increases, but
Nat.rec
isn't applicable because the index that increases is the function's argument. Here, there is a measure of termination that decreases at each recursive call, but the measure is not itself an argument to the function. In these cases, well-founded recursion can be used to define the function. Well-founded recursion is a technique for systematically transforming recursive functions with a decreasing measure into recursive functions over proofs that every sequence of reductions to the measure eventually terminates at a minimum. Applications of functions defined via well-founded recursion are not necessarily definitionally equal to their return values, but this equality can be proved as a proposition. Even when definitional equalities exist, these functions are frequently slow to compute with because they require reducing proof terms that are often very large.- Partial functions with nonempty ranges
For many applications, it's not important to reason about the implementation of certain functions. A recursive function might be used only as part of the implementation of proof automation steps, or it might be an ordinary program that will never be formally proved correct. In these cases, the Lean kernel does not need either definitional or propositional equalities to hold for the definition; it suffices that soundness is maintained. Functions marked
Lean.Parser.Command.declaration : command
partial
are treated as opaque constants by the kernel and are neither unfolded nor reduced. All that is required for soundness is that their return type is inhabited. Partial functions may still be used in compiled code as usual, and they may appear in propositions and proofs; their equational theory in Lean's logic is simply very weak.- Unsafe recursive definitions
Unsafe definitions have none of the restrictions of partial definitions. They may freely make use of general recursion, and they may use features of Lean that break assumptions about its equational theory, such as primitives for casting (
unsafeCast
), checking pointer equality (ptrAddrUnsafe
), and observing reference counts (isExclusiveUnsafe
). However, any declaration that refers to an unsafe definition must itself be markedLean.Parser.Command.declaration : command
unsafe
, making it clear when logical soundness is not guaranteed. Unsafe operations can be used to replace the implementations of other functions with more efficient variants in compiled code, while the kernel still uses the original definition. The replaced function may be opaque, which results in the function name having a trivial equational theory in the logic, or it may be an ordinary function, in which case the function is used in the logic. Use this feature with care: logical soundness is not at risk, but the behavior of programs written in Lean may diverge from their verified logical models if the unsafe implementation is incorrect.
As described in the overview of the elaborator's output, elaboration of recursive functions proceeds in two phases:
-
The definition is elaborated as if Lean's core type theory had recursive definitions. Aside from using recursion, this provisional definition is fully elaborated. The compiler generates code from these provisional definitions.
-
A termination analysis attempts to use the four techniques to justify the function to Lean's kernel. If the definition is marked
Lean.Parser.Command.declaration : command
unsafe
orLean.Parser.Command.declaration : command
partial
, then that technique is used. If an explicitLean.Parser.Command.declaration : command
termination_by
clause is present, then the indicated technique is the only one attempted. If there is no such clause, then the elaborator performs a search, testing each parameter to the function as a candidate for structural recursion, and attempting to find a measure with a well-founded relation that decreases at each recursive call.
This section describes the four techniques in detail.
4.4.1. Structural Recursion
Structurally recursive functions are those in which each recursive call is on a structurally smaller term than the argument. Structural recursion is stronger than the primitive recursion that recursors provide, because the recursive call can use an arbitrary sub-term of the argument, rather than only an immediate sub-term. The constructions used to implement structural recursion are, however, implemented using the recursor; these helper constructions are described in the section on inductive types.
Recursion vs Recursors
Addition of natural numbers can be defined via recursion on the second argument. This function is straightforwardly structurally recursive.
def add (n : Nat) : Nat → Nat
| .zero => n
| .succ k => .succ (add n k)
Defined using Nat.rec
, it is much further from the notations that most people are used to.
def add' (n : Nat) :=
Nat.rec (motive := fun _ => Nat)
n
(fun k soFar => .succ soFar)
Structural recursive calls made on data that isn't the immediate child of the function parameter requires either creativity or a complex yet systematic encoding.
def half : Nat → Nat
| 0 | 1 => 0
| n + 2 => half n + 1
One way to think about this function is as a structural recursion that flips a bit at each call, only incrementing the result when the bit is set.
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
#eval [0, 1, 2, 3, 4, 5, 6, 7, 8].map half'
[0, 0, 1, 1, 2, 2, 3, 3, 4]
Instead of creativity, a general technique called course-of-values recursion can be used.
Course-of-values recursion uses helpers that can be systematically derived for every inductive type, defined in terms of the recursor; Lean derives them automatically.
For every Nat
n
, the type n.below (motive := mot)
provides a value of type mot k
for all k < n
, represented as an iterated dependent pair type.
The course-of-values recursor Nat.brecOn
allows a function to use the result for any smaller Nat
.
Using it to define the function is inconvenient:
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
The function is marked Lean.Parser.Command.declaration : command
noncomputable
because the compiler doesn't support generating code for course-of-values recursion, which is intended for reasoning rather that efficient code.
The kernel can still be used to test the function, however:
#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]
The dependent pattern matching in the body of half''
can also be encoded using recursors (specifically, Nat.casesOn
), if necessary:
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))
This definition still works.
#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]
However, it is now far from the original definition and it has become difficult for most people to understand. Recursors are an excellent logical foundation, but not an easy way to write programs or proofs.
Recognizing structural recursion involves the following steps:
-
The function's parameters are divided into a fixed prefix of parameters that do not vary in any recursive calls, and ordinary parameters that do.
-
The ordinary parameters are split into groups of parameters that, together, may constitute a structurally decreasing parameter. In this step, indices are grouped with the arguments whose types depend on them.
The structural recursion analysis attempts to translate the recursive pre-definition into a use of the appropriate structural recursion constructions.
At this step, pattern matching has already been translated into the use of matcher functions; these are treated specially by the termination checker.
Next, for each group of parameters, a translation using brecOn
is attempted.
Course-of-Values Tables
This definition is equivalent to List.below
:
def List.below' {α : Type u} {motive : List α → Sort u} : List α → Sort (max 1 u)
| [] => PUnit
| _ :: xs => motive xs ×' xs.below' (motive := motive)
In other words, for a given motive, List.below'
is a type that contains a realization of the motive for all suffixes of the list.
More recursive arguments require further nested iterations of the product type. For instance, binary trees have two recursive occurrences.
inductive Tree (α : Type u) : Type u where
| leaf
| branch (left : Tree α) (val : α) (right : Tree α)
It's corresponding course-of-values table contains the realizations of the motive for all subtrees:
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)
For both lists and trees, the brecOn
operator expects just a single case, rather than one per constructor.
This case accepts a list or tree along with a table of results for all smaller values; from this, it should satisfy the motive for the provided value.
Dependent case analysis of the provided value automatically refines the type of the memo table, providing everything needed.
The following definitions are equivalent to List.brecOn
and Tree.brecOn
, respectively.
The primitive recursive helpers List.brecOnTable
and Tree.brecOnTable
compute the course-of-values tables along with the final results, and the actual definitions of the brecOn
operators simply project out the result.
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
The below
construction is a mapping from each value of a type to the results of some function call on all smaller values; it can be understood as a memoization table that already contains the results for all smaller values.
Recursors expect an argument for each of the inductive type's constructors; these arguments are called with the constructor's arguments (and the result of recursion on recursive parameters) during ι-reduction.
The course-of-values recursion operator brecOn
, on the other hand, expects just a single case that covers all constructors at once.
This case is provided with a value and a below
table that contains the results of recursion on all values smaller than the given value; it should use the contents of the table to satisfy the motive for the provided value.
If the function is structurally recursive over a given parameter (or parameter group), then the results of all recursive calls will be present in this table already.
When the body of the recursive function is transformed into an invocation of brecOn
on one of the function's parameters, the parameter and its course-of-values table are in scope.
The analysis traverses the body of the function, looking for recursive calls.
If the parameter is matched against, then its occurrences in the local context are generalized and then instantiated with the pattern; this is also true for the type of the course-of-values table.
Typically, this pattern matching results in the type of the course-of-values table becoming more specific, which gives access to the recursive results for smaller values.
When an recursive occurrence of the function is detected, the course-of-values table is consulted to see whether it contains a result for the argument being checked.
If so, the recursive call can be replaced with a projection from the table.
If not, then the parameter in question doesn't support structural recursion.
Elaboration Walkthrough
The first step in walking through the elaboration of half
is to manually desugar it to a simpler form.
This doesn't match the way Lean works, but its output is much easier to read when there are fewer OfNat
instances present.
This readable definition:
def half : Nat → Nat
| 0 | 1 => 0
| n + 2 => half n + 1
can be rewritten to this somewhat lower-level version:
def half : Nat → Nat
| .zero | .succ .zero => .zero
| .succ (.succ n) => half n |>.succ
The elaborator begins by elaborating a pre-definition in which recursion is still present but the definition is otherwise in Lean's core type theory. Turning on the compiler's tracing of pre-definitions, as well as making the pretty printer more explicit, makes the resulting pre-definition visible:
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
The returned trace message is:
[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)
The auxiliary match function's definition is:
#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
Formatted more readably, this definition is:
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
In other words, the specific configuration of patterns used in half
are captured in half.match_1
.
This definition is a more readable version of half
's pre-definition:
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
To elaborate it as a structurally recursive function, the first step is to establish the bRec
invocation.
The definition must be marked Lean.Parser.Command.declaration : command
noncomputable
because Lean does not support code generation for recursors such as Nat.brecOn
.
noncomputable
def half'' : Nat → Nat :=
fun (x : Nat) =>
x.brecOn fun n table =>
_
/- 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
-/
The next step is to replace occurrences of x
in the original function body with the n
provided by brecOn
.
Because table
's type depends on x
, it must also be generalized when splitting cases with half.match_1
, leading to a motive with an extra parameter.
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
_
_
_)
table
/- To translate:
(fun _ => 0) -- Case for 0
(fun _ => 0) -- Case for 1
(fun n => Nat.succ (half' n)) -- Case for n + 2
-/
The three cases' placeholders expect the following types:
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
The first two cases in the pre-definition are constant functions, with no recursion to check:
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)
_)
table
/- To translate:
(fun n => Nat.succ (half' n)) -- Case for n + 2
-/
The final case contains a recursive call. It should be translated into a lookup into the course-of-values table. A more readable representation of the last hole's type is:
(n : Nat) →
Nat.below (motive := fun _ => Nat) n.succ.succ →
Nat
which is equivalent to
(n : Nat) →
Nat ×' Nat ×' Nat.below (motive := fun _ => Nat) n →
Nat
The first Nat
in the course-of-values table is the result of recursion on n + 1
, and the second is the result of recursion on n
.
The recursive call can thus be replaced by a lookup, and the elaboration is successful:
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
The actual elaborator keeps track of the relationship between the parameter being checked for structural recursion and the positions in the course-of-values tables by inserting sentinel types with fresh names into the motive.
For structural recursion to be detected, a parameter to the function must syntactically be a discriminant of a Lean.Parser.Term.match : term
Pattern 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
.
This maintains the connection between the discriminant and the function parameter, allowing the course-of-values table to line up with the subterms of the original argument.
This connection is checked syntactically: even simple transformations such as wrapping a discriminant and every pattern that matches it with (·, ())
can cause elaboration to fail.
The generalization step that constructs a suitable motive for the auxiliary matchers searches for exact occurrences of the discriminant in the context.
Failing Elaboration
This definition of half
terminates, but this can't be checked by either structural or well-founded recursion.
This is because the gratuitous tuple in the match discriminant breaks the connection between n
and the patterns that match it.
def half (n : Nat) : Nat :=
match (n, ()) with
| (0, ()) | (1, ()) => 0
| (n' + 2, ()) => half n' + 1
fail to show termination for half with errors failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application half 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 n' : Nat ⊢ n' < n
The generalization step that constructs the motive for the auxiliary match functions doesn't connect n
to its patterns, so the course-of-values table doesn't contain a suitable result of recursion.
Similarly, well-founded recursion lacks the connection between the function's parameter and the pattern, though this can be fixed by adding a proposition to the context that states their equality.
This extra information allows the proof automation for well-founded recursion to succeed.
def half (n : Nat) : Nat :=
match h : (n, ()) with
| (0, ()) | (1, ()) => 0
| (n' + 2, ()) =>
-- Here, h : (n, ()) = (n' + 2, ())
have : n = n' + 2 := n✝:Natn:Natn':Nath:(n, ()) = (n'.succ.succ, PUnit.unit)⊢ n = n' + 2 All goals completed! 🐙
half n' + 1
4.4.1.1. Matching Pairs vs Simultaneous Matching
An important consequence of the strategies that are used to prove termination is that simultaneous matching of two discriminants is not equivalent to matching a pair.
Simultaneous matching maintains the connection between the discriminants and the patterns, allowing the pattern matching to refine the types of the assumptions in the local context as well as the expected type of the Lean.Parser.Term.match : term
Pattern 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
.
Essentially, the elaboration rules for Lean.Parser.Term.match : term
Pattern 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
treat the discriminants specially, and changing discriminants in a way that preserves the run-time meaning of a program does not necessarily preserve the compile-time meaning.
Simultaneous Matching vs Matching Pairs for Structural Recursion
This function that finds the minimum of two natural numbers is defined by structural recursion on its first parameter:
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
Replacing the simultaneous pattern match on both parameters with a match on a pair causes termination analysis to fail:
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'
This is because the analysis only considers direct pattern matching on parameters when matching recursive calls to strictly-smaller argument values. Wrapping the discriminants in a pair breaks the connection.
No Structural Recursion Over Pair Components
This function that finds the minimum of the two components of a pair can't be elaborated via structural recursion.
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
This is because the parameter's type's course-of-values recursor is used to justify the recursive definition, but the product type Prod
is not recursive and thus does not have a course-of-values recursor.
This definition is accepted using well-founded recursion, however:
def min (nk : Nat × Nat) : Nat :=
match nk with
| (0, _) => 0
| (_, 0) => 0
| (n' + 1, k' + 1) => min (n', k') + 1
termination_by mk
4.4.1.2. Mutual Structural Recursion
This section will describe the specification of the translation to recursors.
Tracked at issue #56
4.4.2. Well-Founded Recursion
This section will describe the translation of well-founded recursion.
Tracked at issue #57
4.4.3. Controlling Reduction
This section will describe reducibility: reducible, semi-reducible, and irreducible definitions.
Tracked at issue #58
4.4.4. Partial and Unsafe Recursive Definitions
This section will describe partial
and unsafe
definitions:
-
Interaction with the kernel and elaborator
-
What guarantees are there, and what aren't there?
-
How to bridge from unsafe to safe code?
Tracked at issue #59