leanprover / lean3

Lean Theorem Prover
http://leanprover.github.io/
Apache License 2.0
2.15k stars 217 forks source link

Abbreviations in tactic mode #621

Closed robertylewis closed 8 years ago

robertylewis commented 9 years ago

I'm close to finishing the construction of the reals- currently I've shown through commutative ring, with a few holes. The missing theorems have some annoying notation that would be nice to abbreviate.

Currently you can use 'let' to define new terms in tactic proofs, but this is only useful for props, as the data seems to be lost:

example (a b : ℕ) : a = a :=
  begin
    let z := a,
    --have H : z = a, from rfl,  -- this line doesn't work
    apply refl
  end

Is there a way to introduce an abbreviation using let (or something similar), and rewrite the context/goal using the new abbreviation?

leodemoura commented 9 years ago

Unfortunately, this is not supported. Given the goal,

   a b : nat |- a = a

The tactic let z := a is reducing it to the new goal:

  a b : nat, z : nat |- a = a

Suppose we resolve the new goal, and obtain a proof term pr : forall (a b : nat) (z : nat), a = a. Then, the proof term for the original goal is fun (a b : nat), pr a b a.

To do what you want, we would need to add a new kind of hypothesis: z : nat := a. This is a big change since it affects all tactics.

That being said, we can break big theorems in smaller pieces in a section, and define local notation. It would be something like

section big_theorem
   parameters {A : Type} (a b : A) (f : A -> A -> A)
   hypothesis (h : a = b)
   hypothesis ...
   local notation `*` := f
   private lemma aux_lemma_1 (c : A) : a * c = c := ...
   private lemma aux_lemma_2 ...

   theorem my_big_theorem : ... := ...
end big_theorem
robertylewis commented 9 years ago

I see. Thanks for the explanation! I'll find a way to work around it like you suggest.

leodemoura commented 9 years ago

@avigad @fpvandoorn This is an important issue. We should decide whether we support let-expressions in the kernel or not. It is tricky to postpone the decision because it will get harder and harder to implement as the code base increases in size.

Right now, we don't, the let-expressions are "simulated" in the frontend. Jeremy: I don't know if you remember but we discussed this issue in the past. To add let-expressions, we have to:

This is a big change... I'm not excited about doing it, but this is not the first time we had problems because we do not have let-expressions in the kernel.

avigad commented 9 years ago

I remember the discussions. How is "let" handled now? My understanding is that the terms that the kernel sees are fully expanded (though perhaps stored in compressed form, with pointers), but there are annotations in the term that tell the pretty-printer to use a "let" abbreviation. Is that right?

I don't have a strong opinion here. Until this point, I would not have noticed any difference. But I have not used tactics so much yet for long proofs (like Rob is doing). I remember that when working in SSReflect, being able to write "pose" in a tactic proof, ("pose a := foo bar") was helpful.

Can one simulate it somewhat by going into proof-term mode, and using the "let" there?

What are some other problems we have had without "let" in the kernel?

leodemoura commented 9 years ago

I remember the discussions. How is "let" handled now? My understanding is that the terms that the kernel sees are fully expanded (though perhaps stored in compressed form, with pointers), but there are annotations in the term that tell the pretty-printer to use a "let" abbreviation. Is that right?

Exactly.

Can one simulate it somewhat by going into proof-term mode, and using the "let" there?

More or less, the "abbreviation" will only be available in proof-term mode. For example, it will not be available if we return to tactic mode.

What are some other problems we have had without "let" in the kernel?

For example, I had problems with the following kind of declaration

theorem ... :=
  let f := ... in
  assert h : P f, from ...,
  begin
      -- here in tactic mode, f is not available, and all occurrences of f are expanded. 
      -- Example: the of h is not (P f)
  end

The summary is: the current trick we use only works for proof-term mode.

avigad commented 9 years ago

Hmmm. Floris, Jakob, Rob, and you have naturally gravitated towards long tactic proofs, and the proofs we do will get longer, so I guess in the long run it will make a difference.

fpvandoorn commented 9 years ago

I hardly use the let expression, and I haven't missed the ability to abbreviate certain terms in a tactic proof. I do see that this can be useful in long proofs, but I don't think that this is the most important feature to focus attention on right now. I also rarely used this feature in Coq.

mikeshulman commented 8 years ago

For what it's worth, I think this is a very important feature. In older versions of Coq the assert tactic was not transparent, and I found this to be very annoying when doing HoTT. When newer versions of Coq made a transparent assert possible, it made for much cleaner proofs. Separating out using a lemma or section is very annoying when you are in the middle of a long proof with lots of hypotheses.

Similarly, it would be nice to have a transparent version of have for forward reasoning, that could be referred back to with this and backquotes.

fpvandoorn commented 8 years ago

I do want to add change my statement from half a year ago. In the last half year I have missed this feature, and I would use it quite often. I still think it's not top priority, though.

mikeshulman commented 8 years ago

Commit 144b570 that mentions this issue appears to claim that it's adding let-expressions to the kernel. It sounded from the above discussion as though the lack of let-expressions in the kernel was the main obstacle to having a transparent let tactic. If the kernel does now have let-expressions, how much work would it be to add a transparent let tactic?

leodemoura commented 8 years ago

@mikeshulman Commit 144b570 was a failed experiment. The Lean kernel does not have let-expressions in the kernel, and we have decided in a separate mail thread we will not add this kernel extension.

Our plan is to change the representation we use for goals in the tactic framework. We would include a “fake" hypothesis of the form (H : T := V), and label occurrences of the term V as H. Then, we would also have to change tactics to treat occurrences of the labeled term V as an "name". For example, the simplifier and rewriter tactics should not go inside V. We would also have to change the elaborator. It needs to create these fake hypotheses and label terms for the tactic framework. The main advantage of this solution is that we don’t have to change the kernels (C++ and Haskell) The disadvantage is that it is a little bit hackish. We have to make sure all tactics respect the “illusion”. It is not a complicated change, but it is quite laborious. This change is on our TODO list, but it is not the main priority right now. @dselsam and I are currently working full-time on automation for Lean.

mikeshulman commented 8 years ago

@leodemoura Thanks for the explanation! Would this or something like it also allow transparent have?

leodemoura commented 8 years ago

Would this or something like it also allow transparent have?

Yes, we will be able to use let (as a transparent have) in proof-term and tactic modes.

Kha commented 8 years ago

@leodemoura I'm trying to understand the issues of the current implementation, but I can't reproduce the problems with entering tactic mode, at least not in a simple example:

example (A B) (f : A → B) (x : A) : B :=
  let y := f x in
  begin
    apply y
  end

While it's true that y is unfolded in the tactics block, this seems to be equally true in proof-term mode: have h : y = y, from _, shows f x = f x as the current goal.

fpvandoorn commented 8 years ago

The problem is with the let tactic. Compare:

example (A B) (f : A → B) (x : A) : f x = f x :=
let y := f x in
eq.refl y

example (A B) (f : A → B) (x : A) : f x = f x :=
begin 
  let y := f x,
  exact eq.refl y 
end
leodemoura commented 8 years ago

There is also one problem that affects the let-term: it is eagerly expanded.

example (a b : nat) : a + b = 0 -> a = 0 :=
assume H, 
let      c := a + b in
have  Hc : c = 0, from H,
begin
   state  -- in the proof state we have Hc : a + b = 0 instead of Hc : c = 0
end
fpvandoorn commented 8 years ago

Now that I think about it, the following does work:

example (A B) (f : A → B) (x : A) : f x = f x :=
begin 
  exact 
  let y := f x in
  begin exact eq.refl y end
end

Maybe we can (temporarily) make the tactic let y : A := t be syntactic sugar for (or act the same as)

exact let y : A := t in begin [...] end
Kha commented 8 years ago

@fpvandoorn I should have clarified that I was referencing Leo's let-term example (if only there was an easy way of quoting Github comments). Either way, the conclusion I wanted to draw is the same as yours: If the term let also works in tactic mode, it seems like there should be a way to make the tactic let semantically equivalent. Your workaround is great, but I think it will break if there are multiple active subgoals.

@leodemoura Coming from Isabelle/Isar, I am actually used to exactly that behavior. But I see that the tactic let is also used to introduce hypotheses that are then rewritten, so I suppose we cannot just make it syntactically transparent. Considering that, wouldn't the issue be solved by introducing a new tactic command that behaves like the term let?

leodemoura commented 8 years ago

@fpvandoorn the trick exact let y : A := t in begin [...] end is better than nothing. The main problems is that it is still being eagerly expanded. Another minor problem is that it is not really a tactic because it relies on begin ... end (e.g., we cannot use begin ... end inside of an orelse or repeat tacticals). Recall that begin ... end blocks cut the search space. A begin ... end block is a construct for grouping sequences of tactics, where each step is a "checkpoint" (aka Prolog cut). Another problem is that y can only be used in terms embedded inside of the begin ... end. For example, suppose we have

let H : a = 0 := ...,
simp  

The tactic simp would not be able to use H : a = 0, since there is no H : a = 0 in the context. So, it would not rewrite a to 0.

@Kha The current let-tactic is building a proof term of the form (fun y : A, ?M) t, where ?M is a placeholder representing the new proof/term to be synthesized after executing the tactic. In the new context, we have y : A, but we don't have t.

Considering that, wouldn't the issue be solved by introducing a new tactic command that behaves like the term let?

Yes, but we would have to store the the fact y : A := t inside of the new goal.

We have the current proposal:

1- Change the representation for goals in Lean. They would contain sequence of declarations H : A and labeled terms y : A := t. In the current implementation, we have only H : A. The labeled terms would be represented as let-value y (t : A), where let-value is just annotation for (t : A) with the name y. These labeled terms are similar to the annotations we already have in Lean. We implement these extensions using the "macro" expressions supported by the kernel. These macros are all expanded when we execute the kernel at trust-level 0. For this one, let-value y (t : A) expands to t.

2- Some tactics contains nested terms (e.g., apply and exact). These terms are elaborated using the goal context at tactic execution time. Actually, the argument for these tactics are not really terms, but pre-terms, since they have not been elaborated yet. To elaborate these pre-terms, we set the elaboration context using the goal. Now, we would also have to replace occurrences of y in the pre-term with let-value y (t : A).

3- We have to modify the pretty printer to print let-value y (t : A) as y (this is easy).

4- We have to adjust all existing tactics to take into account the new representation for goals. For example, simp should be able to use a hypothesis introduced using let. Tactics such as rewrite should not "visit" the nested term (t : A) at let-value y (t : A). Moreover, when we rewrite a let-value hypothesis, we would have to "propagate" the change to all its occurrences.

5- We have to modify the type class resolution procedure to consider local instances of the form let-value y (t : A). Right now, the type class resolution procedure ignores local instances declared using let-terms. This issue has been raised in another thread.

There is nothing technically hard, but it is a lot work.

Kha commented 8 years ago

(Oh, I just found out about the r shortcut for quoting comments. Too bad it doesn't recover formatting.)

Considering that, wouldn't the issue be solved by introducing a new tactic command that behaves like the term let?

Yes, but we would have to store the the fact y : A := t inside of the new goal.

I fear I'm leading the discussion in circles, so please stop me if that is the case, but I still don't see why we need to store the fact for a transparent let (assuming we keep the current, opaque let for all other cases). I've thrown together a quick experiment at https://github.com/Kha/lean/commit/43f14482589da8e65fb31cd5e883332caef8bf7f, which, as expected, breaks at the few places where you need an opaque let, but does allow tactic proofs like the following (which wouldn't work with the exact workaround):

example (A) (x : A) : x = x ∧ x = x :=
begin
  apply and.intro,
  let y := x,
  apply (eq.refl y),
  apply (eq.refl y),
end
leodemoura commented 8 years ago

@Kha your proposal is not perfect, but it is simple. It combines the implementation of the have-tactic with the one for let-terms. You are eagerly expanding y in the pre-terms in the current scope at parsing time. Then, when the let-tactic is executed, it adds an opaque y to the context.

It is a good temporary workaround, but it has the following problems.

example (a b : nat) : b = 0 → f a + b = 0 :=
begin
  have Ha : f a = 0, from f_ax a,
  intro Ha,
    rewrite Ha -- Ha here is (Ha : b = 0)
end
leodemoura commented 8 years ago

@Kha I mistakenly claimed your suggestion uses the existing let-tactic. This is not the case. You used the id tactic instead.

So, we also have the problem of missing hypothesis. For example, in the following example simp uses H to simplify. If we replace the have with a let, the H will not be there. So, the let will not really be a "transparent have" since it is not adding any hypothesis to the proof state.

import data.nat
open nat

constant f : nat → nat
axiom f_ax : ∀ n, f n = 0

example (a b : nat) : f a + b = b :=
begin
  have H : f a = 0, from f_ax a,
  simp
end
Kha commented 8 years ago

I understand that two separate tactics could lead to some confusion, but careful documentation may be able to prevent that. So let me sketch a more complete proposal:

leodemoura commented 8 years ago

@Kha I think your proposal is a good workaround. It addresses many issues. I like the note tactic. @fpvandoorn Does it solve the let-problems you have been having in HoTT? I'm wondering if you also need a transparent let for non-trivial proof terms.

the Isabelle developers must have had some good reason not to, but I'm assuming implementing it should not be too hard(?)

@Kha I think they have the same problem we have. To implement the let properly, they would have to revise the tactic framework and how they represent goals and the proof state. It is not conceptually hard, but it is a lot of work.

Kha commented 8 years ago

@Kha I think your proposal is a good workaround. It addresses many issues. I like the note tactic.

Great to hear! If @fpvandoorn agrees, I'd be happy to implement it.

leodemoura commented 8 years ago

@fpvandoorn Here is a potential scenario that may affect you.

begin
   ...
   let H := non-trivial-term,
   ...
   apply f H
   ...
   apply g H
   ...
end

We would be replacing H with the non-trivial-term (at parsing time), and each tactic using H would have to re-elaborate H. Moreover, the result may be different since the elaboration process depends on the context. For proof-term mode, the problem is minimized since we have one big simultaneous elaboration step. Similarly, in the tactic,

   apply f H H 

we have two copies of H, but they would be elaborated only once, since they are solved in the same elaboration round. To be able to communicate the result of the elaboration between different tactics, we would also have to extend the tactic framework.

fpvandoorn commented 8 years ago

I'm not really sure whether this would satisfy my needs. One thing which I would want is that if I write let H := non-trivial-term the term H will also be used in goals.

One question: when I write let H := non-trivial-term, in tactic mode, non-trivial-term will not be elaborated at all? So when non-trivial-term contains x, and I later write f H, the x will refer to whatever x is in the context at that moment, and not the moment I wrote let? That would be confusing to me, but I don't know how it will play out in practice. Is it possible to elaborate non-trivial-term already as much as possible when I wrote the let tactic? Or is that problematic, since then the term can become ill-typed if the context changes?

I'm wondering if you also need a transparent let for non-trivial proof terms.

Yes, I would probably use it for non-trivial proof terms. For example, I might want to define a functor (consisting of 2 functions and 2 proofs) within a let and then proof that this functor has some nice properties (e.g. it's an equivalence). I would mainly use let so that I can more easily input the functor, and the goal will become clearer when the functor is replaced by a variable.

leodemoura commented 8 years ago

One question: when I write let H := non-trivial-term, in tactic mode, non-trivial-term will not be elaborated at all?

Yes, in @Kha patch, it is not. It is not really a tactic, but a macro.

I later write f H, the x will refer to whatever x is in the context at that moment, and not the moment I wrote let?

Yes. This is also the case for the current let terms. This is what I meant when I said: The current let-term (and your suggestion) do not respect dynamic scoping rules in tactic mode.

Is it possible to elaborate non-trivial-term already as much as possible when I wrote the let tactic?

It is possible, but it is not a simple change. It is also possible to refactor to refactor the tactic framework as I suggested above, but it is laborious.

Or is that problematic, since then the term can become ill-typed if the context changes?

Yes. I think a workaround should be used only if it is very simple to implement. @Kha workaround is not perfect, but it is a small change, and it will address simple cases.

fpvandoorn commented 8 years ago

Yes. I think a workaround should be used only if it is very simple to implement. @Kha workaround is not perfect, but it is a small change, and it will address several cases.

I agree. It does indeed address several cases, so I think we should try it.

leodemoura commented 8 years ago

@Kha has implemented the proposal above, and it has been integrated in the main branch. https://github.com/leanprover/lean/commit/2185ee7e95f27f190b5e3cd26124ef43e684fb73

So, I'm closing this issue. The original problem posted by @rlewis1988 has been addressed. We still have some rough corners (discussed above), but we are currently postponing a permanent solution.

robertylewis commented 8 years ago

This is great, thanks @Kha !

robertylewis commented 8 years ago

@Kha I've come across some strange behavior in the let tactic. I think it would be easy to fix (if you and @leodemoura agree this is the wrong behavior). The following succeeds:

example : true :=
  begin
    let H'' := blah,
    trivial
  end

This one fails where you'd expect ("unknown identifier blah"):

example : true :=
  begin
    let H'' := blah,
    apply H''
  end

Should the argument to let be type checked when the tactic is called? Technically there's nothing really wrong with the first example, but it seems strange.

Kha commented 8 years ago

@rlewis1988 That certainly seems unintuitive, but it is basically the same issue raised by @fpvandoorn above. @leodemoura mentioned that it wouldn't be easy to fix this - and it really would be a workaround on top of a workaround, since it would only be correct when the context between definition and usage of a let doesn't change.

robertylewis commented 8 years ago

Oh, I'm sorry. I thought I looked through the thread to see if this had been brought up, but somehow I missed Floris' post. It's a bit of a shame this is complicated to fix. Once I train myself to use note instead of let, I suspect it won't catch me very often, but it's still quite counterintuitive.