Zulip Chat Archive

Stream: new members

Topic: Exercises in Theorem Proving in Lean


Alex Zhang (Jun 14 2021 at 10:42):

I am trying to do the following exercise from the book: As exercises, we encourage you to develop a notion of composition for partial functions from α to β and β to γ, and show that it behaves as expected.

open option

def my_comp {α β γ: Type*} (f: α  option β) (g: β  option γ): α  option γ :=
λ a, if f a = none then none
     else sorry

reports error

failed to synthesize type class instance for
α : Type ?,
β : Type ?,
γ : Type ?,
f : α  option β,
g : β  option γ,
a : α
 decidable (f a = none)

How can I fix it?

Kevin Buzzard (Jun 14 2021 at 10:46):

You can either fix it by assuming that option beta has decidable equality (e.g. with open_locale classical) or you can do the problem the way you're expected to do it, without using if and instead splitting on the constructors, like the way addition is defined on the naturals: we don't write "a+b = if b = 0 then a else ...", we write

add : nat -> nat -> nat
| a 0 := a
| a (succ b) = ...

Alex Zhang (Jun 14 2021 at 10:52):

It seems that the first approach

open_locale classical

open option

def my_comp {α β γ: Type*} (f: α  option β) (g: β  option γ): α  option γ :=
λ a, if f a = none then none
     else sorry

gives me another error message: command expected for open_locale.

Kevin Buzzard (Jun 14 2021 at 10:52):

import tactic at the top

Kevin Buzzard (Jun 14 2021 at 10:52):

but let me stress that this is not the way you are expected to do the problem.

Alex Zhang (Jun 14 2021 at 10:59):

Yeah Yeah. I am doing the second way.

Kevin Buzzard (Jun 14 2021 at 11:01):

The difference with the two methods is that the "use the equation compiler" method will give you better definitional equalities; "x = if (true thing) then x else y" is not true by definition, it's true because of the theorem if_pos, whereas using the equation compiler you'll get more stuff true by definition (for example x + 0 = x is true for naturals by definition in Lean).

Alex Zhang (Jun 15 2021 at 08:35):

I am trying to do the exercise "We also encourage you to show that bool and nat are inhabited, that the product of two inhabited types is inhabited, and that the type of functions to an inhabited type is inhabited".
But I am not sure what "to show that bool and nat are inhabited" exactly asks me to do. Am I expected to write a theorem claiming this or something else?

Rémy Degenne (Jun 15 2021 at 08:38):

I would say that for the case of nat, the exercise is to replace that sorry:

example : inhabited nat := sorry

Alex Zhang (Jun 15 2021 at 10:49):

Is there a more concise way to prove the following?

example (α β : Type*) (a: inhabited α) (b: inhabited β)
: inhabited (prod α β) :=
inhabited.mk (inhabited.cases_on a (λ a,a), inhabited.cases_on b (λ b,b))

Kevin Buzzard (Jun 15 2021 at 13:52):

example (α β : Type*) [inhabited α] [inhabited β] :
  inhabited (α × β) :=
⟨⟨arbitrary α, arbitrary β⟩⟩

Kevin Buzzard (Jun 15 2021 at 13:54):

inhabited is a class, so the idiomatic thing to do would be to use rectangular brackets and not name the term of type inhabited alpha. Lean gives it some auto-generated name like _inst_37 or something, but more importantly it also adds it to the typeclass system, meaning that functions like arbitrary (which expect to find an inhabited instance using type class inference) will work.

The first pointy brackets is inhabited.mk, the second is prod.mk. This pointy bracket trick is why you see a lot less of mk functions than you might expect.

Alex Zhang (Jun 15 2021 at 14:31):

Thanks a lot!

Kevin Buzzard (Jun 15 2021 at 14:43):

example (α β : Type*) [inhabited α] [inhabited β] :
  inhabited (α × β) :=
⟨⟨default α, default β⟩⟩

I have no idea which is the best proof.

Mario Carneiro (Jun 15 2021 at 16:27):

example (α β : Type*) :  [inhabited α] [inhabited β], inhabited (α × β)
| a b := ⟨(a, b)⟩

Mario Carneiro (Jun 15 2021 at 16:28):

kevin's is probably the best proof though

Eric Wieser (Jun 15 2021 at 17:28):

docs#default is usually better than docs#arbitrary, I think

Alex Zhang (Jun 16 2021 at 06:02):

Mario Carneiro said:

example (α β : Type*) :  [inhabited α] [inhabited β], inhabited (α × β)
| a b := ⟨(a, b)⟩

Could you please explain the syntax of | ⟨a⟩ ⟨b⟩ ?

Kevin Buzzard (Jun 16 2021 at 06:04):

It's the same syntax as

def add : nat -> nat -> nat
| a 0 := a
|a (succ b) := succ (a + b)

It's explained in chapter 8 (IIRC) of #tpil

Mario Carneiro (Jun 16 2021 at 06:04):

it's a pattern match on inhabited A, which is an inductive type wrapping a single element of type A

Alex Zhang (Jun 19 2021 at 08:46):

''Under the hood, this definition is compiled down to a single inductive type with an index i in a two-valued type (such as bool), where i encodes which of even or odd is intended. ''

mutual inductive even, odd
with even :   Prop
| even_zero : even 0
| even_succ :  n, odd n  even (n + 1)
with odd :   Prop
| odd_succ :  n, even n  odd (n + 1)

What is the single inductive type it is compiled down to?

Horatiu Cheval (Jun 19 2021 at 09:08):

I think the output of #print even._mut_ is the type that definition compiles to

Horatiu Cheval (Jun 19 2021 at 09:11):

Of course, that's not very human-friendly. You can write that inductive using a bool index like this

inductive even_odd : bool    Prop
| even_zero {even : bool} : even  even_odd tt 0
| even_succ {even : bool} :  n, even_odd ff n  even_odd tt (n + 1)
| odd_succ {even : bool} :  n, even_odd tt n  even_odd ff (n + 1)

Horatiu Cheval (Jun 19 2021 at 09:13):

Basically, you write even_odd tt for even and even_odd ff for odd

Kevin Buzzard (Jun 19 2021 at 12:06):

We tend to stay away from mutual inductives in Lean. The reason is this: whilst it no doubt gets compiled down to a single inductive type, it is almost certainly nowhere near as nice as the clean version which Horatiu has posted. Mario's recommendation is always to do the compiling-down yourself. Fortunately our experience in mathematics is that it is quite rare that one needs mutual inductive types, indeed it's surprisingly rare to need inductive types other than structures at all, at least in the kind of mathematics which I have been doing in Lean. Nat itself is a very weird outlier. Even and odd don't need to be defined mutually, and indeed in mathlib they are defined independently.

Alex Zhang (Jun 20 2021 at 08:36):

mutual inductive even, odd
with even :   Prop
| even_zero : even 0
| even_succ :  n, odd n  even (n + 1)
with odd :   Prop
| odd_succ :  n, even n  odd (n + 1)

-- BEGIN
open even odd

theorem not_odd_zero : ¬ odd 0.

Why does this theorem can be closed by a period?

Kevin Buzzard (Jun 20 2021 at 09:24):

Because the equation compiler, which basically knows something about induction, can do enough calculations dealing with cases itself to realise that no cases need to be dealt with by the human

Kevin Buzzard (Jun 20 2021 at 09:28):

The logic is this: we need to prove false given an input of odd 0. That term of type odd 0 needs to come from a constructor of odd. The only constructor of odd ultimately makes a term of type odd (n+1) so before we figure out exactly what to ask the user for we had better find all solutions of n+1=0. The definition of n+1 is succ n so we had better ask the user to deal with all n such that succ n is zero. But these are different constructors for nat so there can't be any solutions at all and so we don't have to ask the user to do anything. Some argument of that nature is happening internally

Alex Zhang (Jun 20 2021 at 10:01):

If I were very stubborn and insist on proving this by myself, what can I do then to close the following proof?

mutual inductive even, odd
with even :   Prop
| even_zero : even 0
| even_succ :  n, odd n  even (n + 1)
with odd :   Prop
| odd_succ :  n, even n  odd (n + 1)

-- BEGIN
open even odd

theorem not_odd_zero : ¬ odd 0 :=
begin
  intros h,
  sorry
end

Eric Wieser (Jun 20 2021 at 10:52):

cases h would be the next step

Kevin Buzzard (Jun 20 2021 at 11:26):

and presumably the last step?

Alex Zhang (Jun 20 2021 at 14:07):

Yes!

Alex Zhang (Jun 20 2021 at 14:23):

This is an example copied from the book:

universe u

inductive vector (α : Type u) :   Type u
| nil {} : vector 0
| cons   : Π {n}, α  vector n  vector (n+1)

namespace vector
local notation h :: t := cons h t

variable {α : Type u}

-- BEGIN
def add [has_add α] : Π {n : }, vector α n  vector α n  vector α n
| .(0)   nil                nil        := nil
| .(n+1) (@cons .(α) n a v) (cons b w) := cons (a + b) (add v w)
-- END

end vector

In the line | .(n+1) (@cons .(α) n a v) (cons b w) := cons (a + b) (add v w), is there any difference between using .(α) and α?

Kevin Buzzard (Jun 20 2021 at 14:53):

If you change .(n+1) to (n+1) then it breaks, so yes. Perhaps the error you get gives you some sort of idea as to the problems going on here.

Kevin Buzzard (Jun 20 2021 at 14:55):

In this case you can get away with (_+1) by the way.

Kevin Buzzard (Jun 20 2021 at 14:57):

I will say again that the dependent type vector n implemented as an inductive type is very hard to work with and whilst it's good to struggle and learn what is hard, when you've finished becoming miserable about how difficult Lean is and how it can't do something as simple as vectors, I would urge you to try everything that you're doing with the subtype definition and discover how easy it all is again.

Alex Zhang (Jun 21 2021 at 04:00):

In the proof of the last theorem, is there a way to match e with the case split in the definition of simp_const? If I do as in the current proof, I will have trouble showing simp_const e = e in the last case.

inductive aexpr : Type
| const :   aexpr
| var :   aexpr
| plus : aexpr  aexpr  aexpr
| times : aexpr  aexpr  aexpr

open aexpr

def aeval (v :   ) : aexpr  
| (const n)    := n
| (var n)      := v n
| (plus e₁ e₂)  := aeval e₁ + aeval e₂
| (times e₁ e₂) := aeval e₁ * aeval e₂

def simp_const : aexpr  aexpr
| (plus (const n₁) (const n₂))  := const (n₁ + n₂)
| (times (const n₁) (const n₂)) := const (n₁ * n₂)
| e                             := e

def fuse : aexpr  aexpr
| (const n)    := const n
| (var n)      := var n
| (plus e₁ e₂)  := simp_const (plus (fuse e₁) (fuse e₂))
| (times e₁ e₂) := simp_const (times (fuse e₁) (fuse e₂))


theorem simp_const_eq (v :   ) :
   e : aexpr, aeval v (simp_const e) = aeval v e :=
λ e, match e with
| (plus (const n₁) (const n₂))  := by simp [aeval,simp_const]
| (times (const n₁) (const n₂)) := by simp [aeval,simp_const]
| e                             := by {have h:simp_const e = e,{sorry},rw h,}
end

Kevin Buzzard (Jun 21 2021 at 08:08):

Instead of match (which does cases) you could use the induction tactic. Does this help?

Alex Zhang (Jun 21 2021 at 08:33):

Thanks for the suggestion, but not quite.. (the actual problem is concerning using the third case in the defn of simp_const as the sorry indicates in the above code)

Alex Zhang (Jun 21 2021 at 08:40):

In the third case of the proof, Lean seems not clever enough to know that e is not in the first two cases.

Kevin Buzzard (Jun 21 2021 at 09:43):

Yes that's correct, Lean doesn't know. If you want it to know then you will need to do induction on e. But it is not clear to me that you are proving the right thing. My instinct is that you should just prove "the big theorem" following e.g. the example at the beginning of chapter 2 of "Certified Programming with Dependent Types" by Chlipala (it's written for Coq but a very early exercise I did in Lean was to port this example over to Lean) and then I would imagine that you will be able to deduce everything relatively easily.

If there's one thing I've learnt from examples like this, it's that this kind of structural induction is much harder than a mathematician imagines. Fortunately I also learnt that there are plenty of computer scientists around here who know the tricks, and also that in practice in the kind of mathematics done by most working mathematicians, this sort of complex induction procedure is rarely if ever needed.

Kevin Buzzard (Jun 21 2021 at 09:47):

If you really want to push this forward, then you need to remember that every definition comes with a cost, and the cost is that you have to make an API for the definition. You have made four definitions without any API at all, and are now trying to prove a theorem relating three of them. What you should be doing is that the moment you have defined simp_const (which, as I've said above, does not look to me like a useful definition, you really want the theorem which says "my compiler is correct" rather than some weaker partial versions) you need to make an API for it, proving things like simp_const (var n) = var n and many other things (you will have to deal with the case simp_const (plus (const n) (times e f)) etc etc, it will be horrible). Once you have a robust API for simp_const you can press on. Every definition you make, you need to pay for.

Kevin Buzzard (Jun 21 2021 at 09:49):

Oh I see -- this is an actual exercise in TPIL? :-/ Then probably what I'm saying is inaccurate. Apart from the bit when I said "I worked through the example in CPDT and it taught me a lot".

Kevin Buzzard (Jun 21 2021 at 09:53):

So yes, on further experimentation, you can absolutely do this by doing induction on everything. I don't see how to do it using the method you're using.

Kevin Buzzard (Jun 21 2021 at 09:57):

Solution

Alex Zhang (Jun 21 2021 at 10:31):

Thanks for your long explanation, Kevin! Could you let me know what "API" here stands for?

Alex Zhang (Jun 21 2021 at 10:34):

A shorter solution using repeat:

theorem simp_const_eq (v :   ) :
   e : aexpr, aeval v (simp_const e) = aeval v e :=
begin
  intro e,
  induction e with _ _ a b _ _ a b _ _,
  repeat { refl },
  repeat { cases a, cases b, repeat { refl }},
end

Kevin Buzzard (Jun 21 2021 at 10:53):

Yes, I thought I'd leave the refls all in there just to show you that at the end of the day we really did have to take it apart into cases. API means "interface". For example you make the reals real as Dedekind cuts or Cauchy sequences (who knows which, the user is not supposed to know) and then you define real.add and real.mul and real.a_non_empty_bounded_set_has_a_LUB and real.field and the proofs of course depend on the definition but the user doesn't look at the proofs. The definition (real) comes with the cost (proving it's a complete archimedean ordered field) and then the user doesn't look at the innards of the definition but just uses the interface you've made for them.


Last updated: Dec 20 2023 at 11:08 UTC