The general syntax for mutual recursion is:
command ::= ... | mutual declaration* end
where the declarations must be definitions or theorems.
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 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.
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.
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 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 marked Lean.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
or Lean.Parser.Command.declaration : command
partial
, then that technique is used.
If an explicit Lean.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 rules that govern recursive functions. After a description of mutual recursion, each of the four kinds of recursive definitions is specified, along with the tradeoffs between reasoning power and flexibility that go along with each.
Just as a recursive definition is one that mentions the name being defined in the body of the definition, mutually recursive definitions are definitions that may be recursive or mention one another. To use mutual recursion between multiple declarations, they must be placed in a mutual block.
The general syntax for mutual recursion is:
command ::= ... | mutual declaration* end
where the declarations must be definitions or theorems.
The declarations in a mutual block are not in scope in each others' signatures, but they are in scope in each others' bodies. Even though the names are not in scope in signatures, they will not be inserted as auto-bound implicit parameters.
Names defined in a mutual block are not in scope in each others' signatures.
mutual
abbrev NaturalNum : Type := Nat
def n : NaturalNum := 5
end
unknown identifier 'NaturalNum'
Without the mutual block, the definition succeeds:
abbrev NaturalNum : Type := Nat
def n : NaturalNum := 5
Names defined in a mutual block are not in scope in each others' signatures. Nonetheless, they cannot be used as automatic implicit parameters:
mutual
abbrev Ξ± : Type := Nat
def identity (x : Ξ±) : Ξ± := x
end
unknown identifier 'Ξ±'
With a different name, the implicit parameter is automatically added:
mutual
abbrev Ξ± : Type := Nat
def identity (x : Ξ²) : Ξ² := x
end
Elaborating recursive definitions always occurs at the granularity of mutual blocks, as if there were a singleton mutual block around every declaration that is not itself part of such a block.
Local definitions introduced via Lean.Parser.Term.letrec : term
let rec
and
Lean.Parser.Command.declaration : command
where
are lifted out of their context, introducing parameters for captured free variables as necessary, and treated as if they were separate definitions within the Lean.Parser.Command.mutual : command
mutual
block as well. Explain this mechanism in more detail, here or in the term section.
Thus, helpers defined in a Lean.Parser.Command.declaration : command
where
block may use mutual recursion both with one another and with the definition in which they occur, but they may not mention each other in their type signatures.
After the first step of elaboration, in which definitions are still recursive, and before translating recursion using the techniques above, Lean identifies the actually (mutually) recursive cliquesdefine this term, it's useful among the definitions in the mutual block and processes them separately and in dependency order.
Structurally recursive functions are those in which each recursive call is on a structurally smaller term than the argument. The same parameter must decrease in all recursive calls; this parameter is called the decreasing parameter. Structural recursion is stronger than the primitive recursion that recursors provide, because the recursive call can use more deeply nested sub-terms 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.
The rules that govern structural recursion are fundamentally syntactic in nature. There are many recursive definitions that exhibit structurally recursive computational behavior, but which are not accepted by these rules; this is a fundamental consequence of the analysis being fully automatic. Well-founded recursion provides a semantic approach to demonstrating termination that can be used in situations where a recursive function is not structurally recursive, but it can also be used when a function that computes according to structural recursion doesn't satisfy the syntactic requirements.
The function countdown
is structurally recursive.
The parameter n
was matched against the pattern n' + 1
, which means that n'
is a direct subterm of n
in the second branch of the pattern match:
def countdown (n : Nat) : List Nat :=
match n with
| 0 => []
| n' + 1 => n' :: countdown n'
Replacing pattern matching with an equivalent Boolean test and subtraction results in an error:
def countdown' (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
This is because there was no pattern matching on the parameter n
.
While this function indeed terminates, the argument that it does so is based on properties of if, the equality test, and subtraction, rather than being a generic feature of Nat
being an inductive type.
These arguments are expressed using well-founded recursion, and a slight change to the function definition allows Lean's automatic support for well-founded recursion to construct an alternative termination proof.
This version branches on the decidability of propositional equality for Nat
rather than the result of a Boolean equality test:
def countdown' (n : Nat) : List Nat :=
if n = 0 then []
else
let n' := n - 1
n' :: countdown' n'
Here, Lean's automation automatically constructs a termination proof from facts about propositional equality and subtraction. It uses well-founded recursion rather than structure recursion behind the scenes.
Structural recursion may be used explicitly or automatically. With explicit structural recursion, the function definition declares which parameter is the decreasing parameter. If no termination strategy is explicitly declared, Lean performs a search for a decreasing parameter as well as a decreasing measure for use with well-founded recursion. Explicitly annotating structural recursion has the following benefits:
It can speed up elaboration, because no search occurs.
It documents the termination argument for readers.
In situations where structural recursion is explicitly desired, it prevents the accidental use of well-founded recursion.
To explicitly use structural recursion, a function or theorem definition can be annotated with a Lean.Parser.Command.declaration : command
termination_by structural
clause that specifies the decreasing parameter.
The decreasing parameter may be a reference to a parameter named in the signature.
When the signature specifies a function type, the decreasing parameter may additionally be a parameter not named in the signature; in this case, names for the remaining parameters may be introduced by writing them before an arrow (Lean.Parser.Command.declaration : command
=>
).
When the decreasing parameter is a named parameter to the function, it can be specified by referring to its name.
def half (n : Nat) : Nat :=
match n with
| 0 | 1 => 0
| n + 2 => half n + 1
termination_by structural n
When the decreasing parameter is not named in the signature, a name can be introduced locally in the Lean.Parser.Command.declaration : command
termination_by
clause.
def half : Nat β Nat
| 0 | 1 => 0
| n + 2 => half n + 1
termination_by structural n => n
The termination_by structural
clause introduces a decreasing parameter.
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
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.
The identifiers before the optional =>
can bring function parameters into scope that are not
already bound in the declaration header, and the mandatory term must indicate one of the function's parameters, whether introduced in the header or locally in the clause.
The decreasing parameter must satisfy the following conditions:
Its type must be an inductive type.
If its type is an indexed family, then all indices must be parameters of the function.
If the inductive or indexed family of the decreasing parameter has data type parameters, then these data type parameters may themselves only depend on function parameters that are part of the fixed prefix.
A fixed parameter is a function parameter that is passed unmodified in all recursive calls and is not an index of the recursive parameter's type. The fixed prefix is the longest prefix of the function's parameters in which all are fixed.
The decreasing parameter's type must be an inductive type.
In notInductive
, a function is specified as the decreasing parameter:
def notInductive (x : Nat β Nat) : Nat := notInductive (fun n => x (n+1))
termination_by structural x
cannot use specified parameter for structural recursion: its type is not an inductive
If the decreasing parameter is an indexed family, all the indices must be variables.
In constantIndex
, the indexed family Fin'
is instead applied to a constant value:
inductive Fin' : Nat β Type where
| zero : Fin' (n+1)
| succ : Fin' n β Fin' (n+1)
def constantIndex (x : Fin' 100) : Nat := constantIndex .zero
termination_by structural x
cannot use specified parameter for structural recursion: its type Fin' is an inductive family and indices are not variables Fin' 100
The parameters of the decreasing parameter's type must not depend on function parameters that come after varying parameters or indices.
In afterVarying
, the fixed prefix is empty, because the first parameter n
varies, so p
is not part of the fixed prefix:
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
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.
Furthermore, every recursive call of the functions must be on a strict sub-term of the decreasing parameter.
The decreasing parameter itself is a sub-term, but not a strict sub-term.
If a sub-term is the 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
expression or other pattern-matching syntax, the pattern that matches the discriminant is a sub-term in the right-hand side of each match alternative.
In particular, the rules of match generalization are used to connect the discriminant to the occurrences of the pattern term in the right-hand side; thus, it respects definitional equality.
The pattern is a strict sub-term if and only if the discriminant is a strict sub-term.
If a sub-term is a constructor applied to arguments, then its recursive arguments are strict sub-terms.
In the following example, the decreasing parameter n
is matched against the nested pattern .succ (.succ n)
. Therefore .succ (.succ n)
is a (non-strict) sub-term of n
, and consequently both n
and .succ n
are strict sub-terms, and the definition is accepted.
def fib : Nat β Nat
| 0 | 1 => 1
| .succ (.succ n) => fib n + fib (.succ n)
termination_by structural n => n
For clarity, this example uses .succ n
and .succ (.succ n)
instead of the equivalent Nat
-specific n+1
and n+2
.
Link to where this special syntax is documented.
In the following example, the decreasing parameter n
is not directly the discriminant 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
expression.
Therefore, n'
is not considered a sub-term of 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'
Using well-founded recursion, and explicitly connecting the discriminant to the pattern of the match, this definition can be accepted.
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 + 1β’ n' < n' + 1 + 1; All goals completed! π
Similarly, the following example fails: although xs.tail
would reduce to a strict sub-term of xs
, this is not visible to Lean according to the rules above.
In particular, xs.tail
is not definitionally equal to a strict sub-term of xs
.
def listLen : List Ξ± β Nat
| [] => 0
| xs => listLen xs.tail + 1
termination_by structural xs => xs
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.
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.
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
failed to infer structural recursion: Cannot use parameter nk: the type Nat Γ Nat does not have a `.brecOn` recursor
This is because the parameter's type, Prod
, is not recursive.
Thus, its constructor has no recursive parameters that can be exposed by pattern matching.
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 nk
Even though the recursive occurrence of countdown
is applied to a term that is not a strict sub-term of the decreasing parameter, the following definition is accepted:
def countdown (n : Nat) : List Nat :=
match n with
| 0 => []
| n' + 1 => n' :: countdown (n' + 0)
termination_by structural n
This is because n' + 0
is definitionally equal to n'
, which is a strict sub-term of n
.
Sub-terms that result from pattern matching are connected to the discriminant using the rules for match generalization, which respect definitional equality.
In countdown'
, the recursive occurrence is applied to 0 + n'
, which is not definitionally equal to n'
because addition on natural numbers is structurally recursive in its second parameter:
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')
Lean supports the definition of mutually recursive functions using structural recursion.
Mutual recursion may be introduced using a mutual block, but it also results from Lean.Parser.Term.letrec : term
let rec
expressions and Lean.Parser.Command.declaration : command
where
blocks.
The rules for mutual structural recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the elaboration steps for mutual groups.
If every function in the mutual group has a termination_by structural
annotation indicating that functionβs decreasing argument, then structural recursion is used to translate the definitions.
The requirements on the decreasing argument above are extended:
All the types of all the decreasing arguments must be from the same inductive type, or more generally from the same mutual group of inductive types.
The parameters of the decreasing parameter's types must be the same for all functions, and may depend only on the common fixed prefix of function arguments.
The functions do not have to be in a one-to-one correspondence to the mutual inductive types. Multiple functions can have a decreasing argument of the same type, and not all types that are mutually recursive with the decreasing argument need have a corresponding function.
The following example demonstrates mutual recursion over a non-mutual inductive data type:
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
The following example demonstrates recursion over mutually inductive types.
The functions Exp.size
and App.size
are mutually recursive.
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
The definition of App.numArgs
is structurally recursive over type App
.
It demonstrates that not all inductive types in the mutual group need to be handled.
def App.numArgs : App β Nat
| .fn _ => 0
| .app a _ => a.numArgs + 1
termination_by structural a => a
Describe mutual structural recursion over nested inductive types.
Tracked at issue #235
If no termination_by
clauses are present in a recursive or mutually recursive function definition, then Lean attempts to infer a suitable structurally decreasing argument, effectively by trying all suitable parameters in sequence.
If this search fails, Lean then attempts to infer well-founded recursion.
For mutually recursive functions, all combinations of parameters are tried, up to a limit to avoid combinatorial explosion.
If only some of the mutually recursive functions have termination_by structural
clauses, then only those parameters are considered, while for the other functions all parameters are considered for structural recursion.
A termination_by?
clause causes the inferred termination annotation to be shown.
It can be automatically added to the source file using the offered suggestion or code action.
Lean automatically infers that the function half
is structurally recursive.
The termination_by?
clause causes the inferred termination annotation to be displayed, and it can be automatically added to the source file with a single click.
def half : Nat β Nat
| 0 | 1 => 0
| n + 2 => half n + 1
termination_by?
Try this: termination_by structural x => x
In this section, the construction used to elaborate structurally recursive functions is explained in more detail.
This elaboration uses the below
and brecOn
constructions that are automatically generated from inductive types' 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 xref sigma 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.
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.
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.
The notion of βsmaller valueβ that is expressed in the below
construction corresponds directly to the definition of strict sub-terms.
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.
This generalization process implements the rule that patterns are sub-terms of match discriminants.
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.
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:Trace not showing up in serialized info - figure out why so this test can work better, or better yet, add proper trace rendering to Verso
[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 bRecOn
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.
A description of the elaboration of mutually recursive functions
Tracked at issue #56
This section will describe the translation of well-founded recursion.
Tracked at issue #57
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
This section will describe reducibility: reducible, semi-reducible, and irreducible definitions.
Tracked at issue #58