Zulip Chat Archive

Stream: mathlib4

Topic: Logical congruence or weakening tactic


Patrick Massot (Oct 04 2023 at 19:20):

Something that comes up from time is I don' t think we have a tactic that will allow to prove a goal with a complicated logical structure using an expression with the same structure but not quite matching. For instance consider

import Mathlib.Tactic

example (x y : ) (h :  ε > 0,  N : ,  n  N, x + n = y + ε) :
   ε > 0,  N : ,  n  N, x - ε = y  - n := sorry

I want to run a single tactic bringing me to a tactic state with data x y ε N n, assumptions ε > 0 and n ≥ N and goal x + n = y + ε ↔ x - ε = y - n or, even better, goal x + n = y + ε → x - ε = y - n. Various ways of not really achieving this goal are:

example (x y : ) (h :  ε > 0,  N : ,  n  N, x + n = y + ε) :
      ε > 0,  N : ,  n  N, x - ε = y  -n := by
  convert h using 6

  constructor
  all_goals { intro ; linarith}

example (x y : ) (h :  ε > 0,  N : ,  n  N, x + n = y + ε) :
     ε > 0,  N : ,  n  N, x - ε = y  -n := by
  revert h
  apply Iff.mp
  congrm  ε > 0,  N : ,  n  N, ?_

  constructor
  all_goals { intro ; linarith}

example (x y : ) (h :  ε > 0,  N : ,  n  N, x + n = y + ε) :
     ε > 0,  N : ,  n  N, x - ε = y  -n := by
  refine forall_imp ?_ h
  intro
  apply forall_imp
  intro
  apply Exists.imp
  intro
  apply forall_imp
  intro
  apply forall_imp
  intro
  intro
  linarith

An example where the deep goal is not an equivalence would be

lemma inv_succ_lt_all :  ε > 0,  N : ,  n  N, 1 / (n + 1 : ) < ε := by
  convert Metric.tendsto_atTop.mp tendsto_one_div_add_atTop_nhds_0_nat using 0
  simp only [Real.dist_0_eq_abs, fun n :   abs_of_pos (Nat.one_div_pos_of_nat (α := ) (n := n))]

lemma inv_succ_lt_all' :  ε > 0,  N : ,  n  N, 1 / (n + 1 : )  ε := sorry

Proving the second lemma from the first one is extremely frustrating for students (this happened to @Claus Clausen this morning for instance). Of course it's doable, but it's frustrating. A principled way to do it is again

lemma inv_succ_lt_all' :  ε > 0,  N : ,  n  N, 1 / (n + 1 : )  ε := by
  refine forall_imp ?_ inv_succ_lt_all
  intro
  apply forall_imp
  intro
  apply Exists.imp
  intro
  apply forall_imp
  intro
  apply forall_imp
  intro
  intro

  exact LT.lt.le _

But I think this deserves a tactic. One tricky part is to generate names without being too unhygienic.

Kevin Buzzard (Oct 04 2023 at 19:25):

This reminds me of the question I ask every January.

Patrick Massot (Oct 04 2023 at 19:37):

Indeed this is not the first time we have this conversation. I don't know if you remember but back in the old days of the perfectoid spaces project I cooked up a broken version of this tactic.

Kevin Buzzard (Oct 04 2023 at 19:38):

@Jireh Loreaux what do you think of this challenge? :-)

Patrick Massot (Oct 04 2023 at 19:38):

If found it! https://github.com/leanprover-community/lean-perfectoid-spaces/blob/95a6520ce578b30a80b4c36e36ab2d559a842690/src/for_mathlib/uniform_space/group_basis.lean#L13-L25

Kevin Buzzard (Oct 04 2023 at 19:38):

Great, now run it through mathport :-) </joke>

Patrick Massot (Oct 04 2023 at 19:39):

I want a non-broken one, just for a change.

Jireh Loreaux (Oct 04 2023 at 20:04):

I'll give it a shot.

Eric Wieser (Oct 04 2023 at 22:28):

Is this something that could be in the realm of gcongr?

Patrick Massot (Oct 04 2023 at 22:32):

It is similar in spirit but formally doesn't fit.

Eric Wieser (Oct 04 2023 at 23:41):

Even under the viewpoint that implication is "just" ≤ on propositions?

Jireh Loreaux (Oct 12 2023 at 16:03):

@Patrick Massot This is my first real foray into metaprogramming on my own, so I'm sure this has bugs or missing features, and is poorly written, but it seems to work on the examples you provided (find them at the end of the snippet). I can already see a few places for improvement, but I wanted to get your feedback first.

import Mathlib.Tactic

open Lean Expr Meta Qq

lemma and_imp_left_of_imp_imp {p q r : Prop} (h : r  p  q) : r  p  r  q := by tauto

def peelQuantifier (goal : MVarId) (e : Expr) (n : Option Name := none) : MetaM MVarId := do
  goal.withContext do
    let ty : Q(Prop)  whnfR ( inferType e)
    let target : Q(Prop)  whnfR ( goal.getType)
    unless ( isProp ty) && ( isProp target) do
      return goal
    match ty, target with
      | .forallE _ t₁ b₁ _, .forallE n₂ t₂ b₂ c => do
        unless  isDefEq ( whnfR t₁) ( whnfR t₂) do
          return goal
        let all_imp  mkFreshExprMVar <|  withoutModifyingState <| withLocalDecl n₂ c t₂ fun x => do
          let type₁ := b₁.instantiate1 x
          let type₂ := b₂.instantiate1 x
          mkForallFVars #[x] ( mkArrow type₁ type₂)
        goal.assign ( mkAppM ``forall_imp #[all_imp, e])
        let (_, new_goal)  all_imp.mvarId!.intro (n.getD n₂)
        return new_goal
      | ~q( x : $t₁, $p x), ~q( x : $t₂, $q x) => do
        unless  isDefEq ( whnfR t₁) ( whnfR t₂) do
          return goal
        let q : Q($t₁  Prop) := q
        let all_imp : Q(Prop)  mkFreshExprMVar q( x : $t₁, $p x  $q x)
        goal.assign ( mkAppM ``Exists.imp #[all_imp, e])
        let (_, new_goal)  all_imp.mvarId!.intro (n.getD `_)
        return new_goal
      | ~q($r  $p), ~q($r'  $q) => do
        unless  isDefEq q($r) q($r') do
          return goal
        let and_imp  mkFreshExprMVar q($r'  $p  $q)
        goal.assign ( mkAppM ``and_imp_left_of_imp_imp #[and_imp, e])
        let (_, new_goal)  and_imp.mvarId!.intro (n.getD `_)
        return new_goal
      | _, _ => do
        return goal

def peelTacAux (e : TSyntax `ident) (n : Option (TSyntax `ident)) : Elab.Tactic.TacticM Unit :=
  Elab.Tactic.withMainContext do
    let e  Elab.Term.elabTerm e none
    match n with
    | .some n =>
    Elab.Tactic.liftMetaTactic (fun g  return [( peelQuantifier g e (n := .some n.getId))])
    | .none => Elab.Tactic.liftMetaTactic (fun g  return [( peelQuantifier g e)])

syntax (name := peel) "peel" ident (colGt ident)? : tactic
@[tactic peel] def peelTac : Elab.Tactic.Tactic := fun stx => do
  match stx with
  | `(tactic| peel $e:ident) => peelTacAux e none
  | `(tactic| peel $e:ident $n:ident) => peelTacAux e (.some n)
  | _ => Elab.throwUnsupportedSyntax

syntax (name := zoom) "zoom" ident ((colGt ident)*)? : tactic
macro_rules
  | `(tactic| zoom $h:ident) => `(tactic| peel $h; intro h'; try (replace $h := h'; clear h'))
  | `(tactic| zoom $h:ident $n:ident) => `(tactic| peel $h $n; intro h'; try (replace $h := h'; clear h'))
  | `(tactic| zoom $h:ident $n:ident $ns:ident*) => `(tactic| zoom $h $n; zoom $h $ns:ident*)

example (p q : Nat  Prop) (h :  y, p y) (h₁ :  z, p z  q z) :  x, q x := by
  zoom h n
  exact h₁ n h
  done

example (p q : Nat  Prop) (h :  y, p y) (h₁ :  z, p z  q z) :  x, q x := by
  zoom h x
  exact h₁ x h
  done

example (p q : Nat  Prop) (h :  y, p y) (h₁ :  z, p z  q z) :  x, q x := by
  zoom h x
  exact h₁ x h
  done

example (p q :     Prop) (h :  ε > 0,  δ > 0, p ε δ)
    (hpq :  x y, x > 0  y > 0  p x y  q x y) :
     ε > 0,  δ > 0, q ε δ := by
  zoom h ε  δ 
  exact hpq ε δ   h

example (x y : ) (h :  ε > 0,  N : ,  n  N, x + n = y + ε) :
     ε > 0,  N : ,  n  N, x - ε = y  - n := by
  zoom h ε  N n hn
  linarith

lemma inv_succ_lt_all :  ε > 0,  N : ,  n  N, 1 / (n + 1 : ) < ε := by
  convert Metric.tendsto_atTop.mp tendsto_one_div_add_atTop_nhds_0_nat using 0
  simp only [Real.dist_0_eq_abs, fun n :   abs_of_pos (Nat.one_div_pos_of_nat (α := ) (n := n))]

lemma inv_succ_lt_all' :  ε > 0,  N : ,  n  N, 1 / (n + 1 : )  ε := by
  have h := inv_succ_lt_all -- right now `zoom` only accepts `ident`s, not a supplied `term`.
  zoom h ε  N n hn
  exact h.le

Patrick Massot (Oct 12 2023 at 16:11):

This is fantastic! Thanks a lot for working on this.

Patrick Massot (Oct 12 2023 at 16:13):

I think the syntax is not consistent with other tactics. I would expect the with token to introduce the name lists. This will probably make it easier to take in an arbitrary expression instead of the name of a local declaration.

Jireh Loreaux (Oct 12 2023 at 16:15):

Hold on, I think I can do that in a few seconds. Meh, my janky replace business was getting in the way, may take me a bit longer.

Jireh Loreaux (Oct 12 2023 at 18:59):

Okay, I plan to clean it up and add the following features:

  1. use with, and then allow terms instead of just idents
  2. proper handling of userName by making fresh ones
  3. take an optional numeric argument which allows for setting the depth without specifying variable names

Jireh Loreaux (Oct 12 2023 at 18:59):

Let me know if there's anything else you want it to do.

Patrick Massot (Oct 12 2023 at 19:43):

I don't feel confident enough in meta-programming to review meta-code written by others. But I want to point out a potential trap since you are not yet an expert. I think it's fine in your case, but I always get nervous when I see nested nested actions as in your code. Do you understand why the two functions myFun and myFun' do not return the same result in the code below?

abbrev M := StateT Nat Id

def f : M Nat  M Nat := fun n  do
  set 5
  return  n

def g : Nat  M Nat := fun n  do
  return n + ( get)

def myFun : M Nat := do
  return  f <| g ( get)

#eval myFun.run' 0  -- says 5

def myFun' : M Nat := do
  return  f <| do g ( get)

#eval myFun'.run' 0  -- says 10

Patrick Massot (Oct 12 2023 at 19:44):

Let me tag @David Thrane Christiansen since I don't remember if #fpil warns about that specific version of this footgun.

Vincent Beffara (Oct 12 2023 at 19:48):

This zoom tactic seems very useful! It would probably feel more natural to have roughly the same syntax as filter_upwards though (zoom [h] with n or something of that kind). And possibly make zoom accept eventually as a quantifier ?

Patrick Massot (Oct 12 2023 at 19:49):

I don't understand the last sentence.

Vincent Beffara (Oct 12 2023 at 19:49):

That's because I don't understand filters :-)

Patrick Massot (Oct 12 2023 at 19:50):

Could you give an example of what you would like to see as tactic state before and after applying the tactic?

Vincent Beffara (Oct 12 2023 at 19:50):

The way zoom treats \forall feels a lot like the way filter_upwards treats eventually

Patrick Massot (Oct 12 2023 at 19:51):

Oh I see, you want a version that handle eventually and frequently where the current one handles forall and exists?

Vincent Beffara (Oct 12 2023 at 19:52):

Yes, exactly. They seem to fill the same purpose, might as well be the same tactic

Vincent Beffara (Oct 12 2023 at 19:53):

(I don't "want" it, I am just wondering if it would make sense.)

Vincent Beffara (Oct 12 2023 at 20:00):

example {x y : } {f :   } (h :  x : , ∀ᶠ y in 𝓝 x, |f y - f x|  |y - x|) :
     x : , ∀ᶠ y in 𝓝 x, |(f y - f x) ^ 2|  |y - x| ^ 2 := by
  zoom h x y
/-
f: ℝ → ℝ
x y: ℝ
h: |f y - f x| ≤ |y - x|
⊢ |(f y - f x) ^ 2| ≤ |y - x| ^ 2
-/

Patrick Massot (Oct 12 2023 at 20:07):

Yes, I think this would be a nice extension.

Patrick Massot (Oct 12 2023 at 20:07):

However it would not really replace filter_upwards which can take several arguments.

Vincent Beffara (Oct 12 2023 at 20:12):

zoom [h1, h2] with x ? As in

example (p q : Nat  Prop) (h :  y, p y) (h₁ :  z, p z  q z) :  x, q x := by
  zoom [h, h₁] with x h h₁ using h₁ h
  done

Joachim Breitner (Oct 12 2023 at 20:19):

Judging from the examples, this tactic looks like a variant of intro that also specializes a bunch of hypothesea using the intro'ed terms. Is that correct? I wonder if there is a name other than zoom that conveys this intuition better (or conveys the right intuition if this one is wrong).
Maybe something along the lines of

intros x y specializing h1 h2

Hmm, I guess it does more than just intro, and supports other quantifiers as well. Nevermind that particular suggestion then .

Vincent Beffara (Oct 12 2023 at 20:21):

To be even more similar to filter_upwards, the zoom [h, h₁] with x h h₁ using h₁ h case would feel like this:

example (p q : Nat  Prop) (h :  y, p y) (h₁ :  z, p z  q z) :  x, q x := by
  rw [ Filter.eventually_top] at h h₁ 
  filter_upwards [h, h₁] with x h h₁ using h₁ h

Patrick Massot (Oct 12 2023 at 20:34):

I also think the name isn't great, but I didn't to derail the conversation into bike shedding before we agree on the technical core.

Jireh Loreaux (Oct 12 2023 at 20:58):

Yeah, the zoom name was just a placeholder.

Jireh Loreaux (Oct 12 2023 at 21:11):

Patrick, thanks for that example and warning. I understand why the results are what they are, but I don't yet feel it deep enough in my bones that I would have spotted it without prompting. Do you have a suggestion for how I should be de-nestifying my dos? Just one example of how the code should be transformed would be enough.

Jireh Loreaux (Oct 12 2023 at 21:11):

Adding support for ∀ᶠ and ∃ᶠ should be relatively straightforward.

Patrick Massot (Oct 12 2023 at 21:19):

I don't know if we should de-nestify. You should simply know that if you find an incomprehensible bug where Lean seems to ignore a withWhatever or withoutModifyingWhatever then this issue is probably the cause.

Patrick Massot (Oct 12 2023 at 21:21):

If you want to explicitly say "I know where this nested action will bubble up" then you can simply bubble it yourself, as in:

def myFun : M Nat := do
  let x  get
  return  f <| g x

which is what myFun was doing in my example.

Patrick Massot (Oct 12 2023 at 21:22):

The problem is that what you usually want it the opposite behavior. For this I don't know any alternative to insert the missing do.

Patrick Massot (Oct 12 2023 at 21:22):

Maybe @Kyle Miller wants to comment on this since we hit that issue several times while coding together.

Jireh Loreaux (Oct 14 2023 at 04:47):

I'm mostly happy with this now, so let the bikeshedding begin.

peel tactic

Note that:

  1. the temporary name is now peel
  2. it supports ∀ᶠ and ∃ᶠ now.
  3. It uses with and accepts terms
  4. It allows for a numeric argument and hypotheses will be named automatically, for situations in which the names don't matter.

This means that we can simplify one of the original examples to:

example (x y : ) (h :  ε > 0,  N : ,  n  N, x + n = y + ε) :
     ε > 0,  N : ,  n  N, x - ε = y  - n := by
  peel 5 h
  linarith

Jireh Loreaux (Oct 14 2023 at 04:50):

Oh, and for some reason using Qq really makes compiling the peelQuantifier declaration slow. I'm not sure why though. Perhaps I'm doing something wrong.

Vincent Beffara (Oct 14 2023 at 08:19):

Very nice! So how hard would it be to add

  • possibility to add using h x y at the end to close the goal in one line?
  • support for several inputs peel [h1, h2]?

[Maybe I'm seeing too much similarity with filter_upwards but in fact it seems that merging the two would make sense. From an end-user's point of view.]

Jireh Loreaux (Oct 14 2023 at 11:41):

I'm not sure what several inputs would mean? using is probably easy.

Joachim Breitner (Oct 14 2023 at 12:19):

There could be multiple hypotheses around that you want to peel stuff off together?

Jireh Loreaux (Oct 14 2023 at 14:28):

(deleted)

Timo Carlin-Burns (Oct 14 2023 at 15:35):

If it's given multiple hypotheses to peel stuff off of and they both have have existentials, how would it choose which hypothesis to use the witness from?

example (p :     Prop) (h :  ε > 0,  δ > 0, p ε δ) (h2 :  ε > 0,  δ > 0, p ε δ  q ε δ) :
     ε > 0,  δ > 0, q ε δ :=
  by peel [h, h2] -- ??
  -- this doesn't make sense because there are two conflicting witnesses for δ

Patrick Massot (Oct 14 2023 at 15:36):

The situation is much clearer for the filter version since one of the axioms defining filters precisely adresses this question.

Patrick Massot (Oct 14 2023 at 15:37):

What the filter technology would do here is to choose the minimum of the two witnesses. But of course in the general case there is no clear choice to make.

Patrick Massot (Oct 14 2023 at 15:39):

More explicitly, docs#Filter.Eventually.and is a wrapper around docs#Filter.inter_mem and when you prove that neighborhood of zero form a filter, taking this min is exactly what ensures this axiom.

Patrick Massot (Oct 14 2023 at 15:39):

So I guess my answer is: for serious uses you need filters anyway and for teaching purposes the current peel is good enough.

Patrick Massot (Oct 14 2023 at 15:40):

And of course the current peel with one argument is also great as a general purpose tool, even in serious contexts.

Patrick Massot (Oct 14 2023 at 15:41):

And note that I know "serious" is not the right word. Teaching is also serious.

Jireh Loreaux (Oct 14 2023 at 19:33):

What about the name? I'm open to suggestions. peel, peel_quantifier, match_quantifier, qcongr, telescope?

Vincent Beffara (Oct 14 2023 at 20:21):

qconvert?

Jireh Loreaux (Oct 15 2023 at 03:03):

#7685

Utensil Song (Oct 15 2023 at 04:55):

peel seems to be a very vivid and encouraging name, matching its semantic, I love it.

Joachim Breitner (Oct 15 2023 at 08:38):

I agree. Although I see the merit of the more sober qconvert, it works a bit like convert with implication rather than equality, doesn't it?


Last updated: Dec 20 2023 at 11:08 UTC