Zulip Chat Archive
Stream: lean4
Topic: fibonacci functions
Joseph O (Mar 06 2022 at 18:05):
So i wrote a fibonacci function like this:
def fib : Nat → Nat
| 0 => 0
| 1 => 1
| n => fib (n-1) + fib (n-2)
and it didn't work because I had to prove termination. i then tried
def fib : Nat → Nat
| 0 => 0
| 1 => 1
| n+1 => fib (n) + fib (n-1)
and the same thing happened, but this version worked:
def fib : Nat → Nat
| 0 => 0
| 1 => 1
| n+2 => fib (n+1) + fib n
I didnt have to prove any termination at all. So my question is, is this a bug or just how the core language works?
Kevin Buzzard (Mar 06 2022 at 18:09):
That's certainly not a bug. Do you know what 0-1 is? If so you can probably work out what's going on.
Joseph O (Mar 06 2022 at 18:11):
Fundamentally, I think i am just not supposed to subtract
Henrik Böving (Mar 06 2022 at 18:17):
The last version of your code is simply the only one that allows lean to automatically apply structural recursion since in every recursive call the parameter you are parsing is structually smaller than the one you got.
Now if you were to e.g. take your second version and apply termination by:
def fib : Nat → Nat
| 0 => 0
| 1 => 1
| n+1 => fib (n) + fib (n-1)
termination_by fib n => n
Lean will complain that:
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 specity your own tactic for discharging this kind of goal
n : Nat
⊢ n - 1 < Nat.succ n
and you'd have to prove this by hand since lean cannot just automagically see whats going on.
Joseph O (Mar 06 2022 at 18:19):
Another solution is to mark it with partial
, but that is generally not really ideal
Henrik Böving (Mar 06 2022 at 18:19):
That's not a solution to prove termination that's telling lean to ignore termination on this one
Joseph O (Mar 06 2022 at 18:22):
I mean not as a solution, but to ignore the termination errors.
Leonardo de Moura (Mar 06 2022 at 18:55):
def fib : Nat → Nat
| 0 => 0
| 1 => 1
| n+1 =>
have : n - 1 < n + 1 := Nat.lt_of_le_of_lt (Nat.sub_le ..) (Nat.lt_succ_self ..) -- This auxiliary prop will be proved automatically in the future
fib n + fib (n-1)
Henrik Böving (Mar 06 2022 at 19:09):
What does the ..
syntax do? @Leonardo de Moura
loki der quaeler (Mar 06 2022 at 19:18):
I believe it's denoting that the arguments can be inferred - see the bottom of this page: https://leanprover.github.io/theorem_proving_in_lean4/interacting_with_lean.html
Henrik Böving (Mar 06 2022 at 19:20):
Ah so a multi _ alias I see
Leonardo de Moura (Mar 06 2022 at 19:50):
I think the ..
notation + named arguments is also quite useful for writing patterns.
inductive Foo
| mk₁ (x y z w : Nat)
| mk₂ (x y z w : Nat)
def Foo.z : Foo → Nat
| mk₁ (z := z) .. => z
| mk₂ (z := z) .. => z
Foo.z
will not break if we add new fields to the constructors.
Kyle Miller (Mar 06 2022 at 19:53):
Joseph O said:
So i wrote a fibonacci function like this:
def fib : Nat → Nat | 0 => 0 | 1 => 1 | n => fib (n-1) + fib (n-2)
and it didn't work because I had to prove termination.
Just to mention what a problem is that prevents you from showing termination here, with match
statements each pattern is independent: the third case doesn't "know" that n
is neither 0
nor 1
.
If match
could somehow give you that information (maybe as n ≠ 0 ∧ n ≠ 1
), then you'd be able to show the function terminates.
Kyle Miller (Mar 06 2022 at 19:56):
You could say the plus patterns (like n+2
) are a way to avoid needing a fancy feature like that while still being relatively convenient.
Mario Carneiro (Mar 06 2022 at 20:12):
Note that lean 4 has that fancy feature, so potentially you could use it to prove the first one is terminating too
Mario Carneiro (Mar 06 2022 at 20:14):
I don't think there is syntax to expose the failing cases to the match expression itself though
Leonardo de Moura (Mar 06 2022 at 20:18):
If match could somehow give you that information (maybe as n ≠ 0 ∧ n ≠ 1), then you'd be able to show the function terminates.
We have support for this only when reasoning about code that uses match
. The split
tactic will add this kind of antecedent.
def foo : Nat → Nat
| 0 => 0
| 1 => 1
| n => n + 2
example : foo (n + 2) ≥ n + 4 := by
unfold foo
split
trace_state -- In the third goal we have extra hypotheses that make sure the previous cases were not taken.
case h_1 h => simp_arith at h
case h_2 h => simp_arith at h
case h_3 h _ _ _ => subst h; simp_arith
Leonardo de Moura (Mar 06 2022 at 20:19):
Mario Carneiro said:
I don't think there is syntax to expose the failing cases to the match expression itself though
Unfortunately, we don't. We only have support for this when reasoning about match
expressions as in the example above.
Joseph O (Mar 06 2022 at 22:49):
Thanks for these great answers! :tada: :+1:
Joseph O (Mar 07 2022 at 01:08):
Not really related to the original question, but why is this not structural recursion
def List.unfoldr (f : β → Option (α × β)) (s : β) : List α :=
match f s with
| none => []
| some (a, b') => a::(unfoldr f b')
nor this
def List.unfoldlAux (f : β → Option (α × β)) (s : β) (acc : List α) : List α :=
match f s with
| none => acc
| some (a, a') => unfoldlAux f a' (a::acc)
and how can i make it structural
Joseph O (Mar 07 2022 at 01:11):
i could just mark them with partial and leave it like that, as these functions are for personal use, but why may they never terminate?
Arthur Paulino (Mar 07 2022 at 01:16):
It's not clear to me why f s
can eventually be none
(I'm guessing that's when you want your functions to terminate)
Mario Carneiro (Mar 07 2022 at 01:20):
To answer the question "why is it not structural recursion": A structural recursion begins by matching on one of the function arguments, and then calling the function only on the values coming out of that match. Here you are matching on f s
, which is not a function argument, and you are calling yourself recursively on b'
which is not a value coming from a match on said function argument
Mario Carneiro (Mar 07 2022 at 01:21):
As arthur says, not only is this not structurally recursive, it's not well founded recursive either, and you can in fact make it loop by passing the function (\lam a, some (0, a))
for f
Joseph O (Mar 07 2022 at 01:25):
Mario Carneiro said:
As arthur says, not only is this not structurally recursive, it's not well founded recursive either, and you can in fact make it loop by passing the function
(\lam a, some (0, a))
forf
but what can i do to match on the argument?
Joseph O (Mar 07 2022 at 01:25):
it wouldnt help
Joseph O (Mar 07 2022 at 01:28):
I feel like for my purpose just marking it with partial
would suffice
Joseph O (Mar 07 2022 at 01:38):
but for the future, how would the structural recursive version look like?
Arthur Paulino (Mar 07 2022 at 01:39):
Wanna describe the problem you're trying to solve verbally on another thread to clear out the #xy noise? I suspect you'll need to rethink the whole solution design because your function can indeed loop forever for some functions f
(e.g. with Mario's example).
Mauricio Collares (Mar 07 2022 at 01:50):
unfoldr
is a useful function in general, but I guess in Lean it should return something like docs#stream instead of a List
Leonardo de Moura (Mar 07 2022 at 03:25):
Joseph O said:
but for the future, how would the structural recursive version look like?
As @Mario Carneiro pointed out your function does not terminate in general. You can change the signature a bit, and ask the user to show that the new b
is "smaller". Here are some examples
def List.unfoldr {α β : Type u} [sz : SizeOf β] (f : (b : β) → Option (α × { b' : β // sizeOf b' < sizeOf b})) (b : β) : List α :=
match f b with
| none => []
| some (a, ⟨b', h⟩) => a :: unfoldr f b'
def tst1 (n : Nat) : List Nat :=
List.unfoldr (b := n) fun
| 0 => none
| b+1 => some (3*n - b*2, ⟨b, by simp_arith⟩)
#eval tst1 10
def tst2 (n : Nat) : List Nat :=
-- Similar example where we provide our custom `SizeOf` instance
List.unfoldr (sz := ⟨fun b => n - b⟩) (b := 0) fun b =>
if h : b < n then
some (b*2, ⟨b+1, by decreasing_tactic⟩)
else
none
#eval tst2 10
-- More general (and less convenient to use) version that can take an arbitrary well-founded relation.
def List.unfoldr' {α β : Type u} [w : WellFoundedRelation β] (f : (b : β) → Option (α × { b' : β // w.rel b' b})) (b : β) : List α :=
match f b with
| none => []
| some (a, ⟨b', h⟩) => a :: unfoldr' f b'
termination_by unfoldr' b => b
-- We need the `master` branch to test the following example
def tst3 (n : Nat) : List Nat :=
List.unfoldr' (b := n) fun
| 0 => none
| b+1 => some (3*n - b*2, ⟨b, by decreasing_tactic⟩)
#eval tst3 10
def tst4 (n : Nat) : List Nat :=
List.unfoldr' (w := invImage (fun b => n - b) inferInstance) (b := 0) fun b =>
if h : b < n then
some (2*b, ⟨b+1, by decreasing_tactic⟩)
else
none
#eval tst4 10
Leonardo de Moura (Mar 07 2022 at 03:29):
If the angle brackets bother you, you can define a macro. Example:
notation "#(" a ")" => ⟨a, by decreasing_tactic⟩
def tst3 (n : Nat) : List Nat :=
List.unfoldr' (b := n) fun
| 0 => none
| b+1 => some (3*n - b*2, #(b))
#eval tst3 10
Siddhartha Gadgil (Mar 07 2022 at 09:12):
Thanks for the very helpful explanations and the updated docs. Can someone confirm my understanding of all the cases?
- To prove termination, we need a well-founded relation and a proof that the argument is decreasing for recursive calls (where decrease is with respect to the given relation).
- If there is no termination_by, a well-founded relation is derived (if possible) by implicit resolution on the arguments of the function. It is an error if the typeclass resolution fails.
- If termination_by is specified, it maps the arguments of the function to a type α and implicit resolution looks for a well-founded relation on α (which we may view as giving a pull-back relation on the original arguments). Again it is an error if the implicit resolution fails, but this time for α .
- In either case, we have to show that arguments for recursive calls are decreasing with respect to the well-founded relation. By default this is done using the decreasing_tactic, with an error if this fails and decreasing_by is not specified. One can instead use decreasing_by to specify the tactic.
- A useful trick to make things elegant is to note that decreasing_tactic includes assumption, so one can include a
have
statement that proves arguments are decreasing, and this gets picked up by assumption. This is especially useful if we have multiple recursive calls, since the appropriatehave
can be added just before each call (this is helpful to lean and to the reader).
Thanks.
Joseph O (Mar 07 2022 at 14:17):
this one doesnt terminate:
def List.unfoldr {α β : Type u} [sz : SizeOf β] (f : (b : β) → Option (α × { b' : β // sizeOf b' < sizeOf b})) (b : β) : List α :=
match f b with
| none => []
| some (a, ⟨b', h⟩) => a :: unfoldr f b'
``` just saying
Joseph O (Mar 07 2022 at 14:18):
and the second version has a type mismatch image.png
Leonardo de Moura (Mar 07 2022 at 14:19):
@Joseph O You need the Lean 4 master branch.
Note that this examples are now in our CI :)
Sebastian Ullrich (Mar 07 2022 at 14:20):
(also please don't quote entire (long) comments)
Joseph O (Mar 07 2022 at 14:23):
Sebastian Ullrich said:
(also please don't quote entire (long) comments)
Ah sorry
Joseph O (Mar 07 2022 at 14:24):
How do i get the master branch? Do i have to clone from source then?
Arthur Paulino (Mar 07 2022 at 14:26):
I usually wait for the next nightly release
Joseph O (Mar 07 2022 at 14:27):
Yeah I would do that. Why do the examples @Leonardo de Moura posted not work on the current nightly version? Was something added?
Leonardo de Moura (Mar 07 2022 at 14:28):
Joseph O said:
How do i get the master branch? Do i have to clone from source then?
Yes, this is an option, but the repo is quite big. Another option is to press the green "Code" button at GitHub, and select "Download Zip". The zip file includes just the master branch. The current nightly build is good enough for part of the examples.
Leonardo de Moura (Mar 07 2022 at 14:28):
Joseph O said:
Yeah I would do that. Why do the examples Leonardo de Moura posted not work on the current nightly version? Was something added?
The current nightly build is missing this commit https://github.com/leanprover/lean4/commit/619186b2a839ebd191257d78595d240e8eab47d7
Leonardo de Moura (Mar 07 2022 at 14:28):
It will probably be there tomorrow.
Leonardo de Moura (Mar 07 2022 at 14:29):
Without this commit, code generation will fail in the example that uses invImage
.
Joseph O (Mar 07 2022 at 14:31):
ok well thank you
Sebastian Ullrich (Mar 07 2022 at 14:31):
Perhaps we should postpone the nightly cut-off time by a few hours so it's night for both of us :grinning:
Sebastian Ullrich (Mar 07 2022 at 14:36):
8AM CET/11PM PT sounds reasonable enough? Now I just need to find out the server timezones of GitHub and of the new bot
Leonardo de Moura (Mar 07 2022 at 14:46):
@Siddhartha Gadgil Thanks for writing the summary. I included it in "Theorem Proving in Lean 4"
Joseph O (Mar 09 2022 at 22:42):
What does sizeOf
do in your function @Leonardo de Moura ?
Leonardo de Moura (Mar 09 2022 at 22:59):
sizeOf
is an auxiliary function we use to prove termination. It has type:
{α : Sort u} → [SizeOf α] → α → Nat
When we declare a new inductive type or structure, Lean automatically generates a SizeOf
instance for this type and equation theorems. For example, here are the theorems generated for List
s.
#check @List.nil.sizeOf_spec
-- ∀ {α : Type u} [inst : SizeOf α], sizeOf [] = 1
#check @List.cons.sizeOf_spec
-- ∀ {α : Type u} [SizeOf α] (head : α) (tail : List α), sizeOf (head :: tail) = 1 + sizeOf head + sizeOf tail
The decreasing_tactic
uses these theorems when proving termination.
For Nat
, the SizeOf
instance is just the identity function.
The short answer is: SizeOf
already exists and was a convenient way of making List.unfoldr
work for different types.
In the List.unfoldr
example, Lean shows termination using the decreasing_tactic
, this tactic succeeds because it also uses the assumption
tactic, and we have h : sizeOf b' < sizeOf b
in the context. In tst1
, when our function returns a new b
, we also show sizeOf b < sizeOf (b+1)
using simpArith
.
Joseph O (Mar 10 2022 at 01:52):
What syntatic sugar are you using in tst1
? Also, i really need to read TPIL4
Joseph O (Mar 10 2022 at 01:52):
Or else im never going to be able to prove termination of my functions
Leonardo de Moura (Mar 10 2022 at 02:29):
Are you referring to ⟨b, by simp_arith⟩
? This is the anonymous constructor notation. We can use it for types that have only one constructor. In this case, it expands to Subtype.mk b (by simp_arith)
You can view a subtype as a pair: the element b
, and a proof that sizeof b < sizeof (b+1)
. The proof is being constructed by the tactic by simp_arith
.
Joseph O (Mar 10 2022 at 02:41):
I was actually referring to
fun
| 0 => none
| b+1 => some (3*n - b*2, ⟨b, by simp_arith⟩)
i shouldve explained myself better, sorry
Leonardo de Moura (Mar 10 2022 at 02:46):
This is sugar for
fun x => match x with
| 0 => none
| b+1 => some (3*n - b*2, ⟨b, by simp_arith⟩)
Joseph O (Mar 10 2022 at 02:47):
ah, like the function
keyword in OCaml.
Last updated: Dec 20 2023 at 11:08 UTC