Zulip Chat Archive

Stream: general

Topic: Why does rfl fail to instantiate metavariables?


Jason Gross (Jun 20 2025 at 03:47):

Can someone explain this error to me?

def construct_by_refinement :  (result : Nat), result = 5 := by
  refine ⟨?result, ?pf
  case pf => rfl

What's the right way to construct metavariables and later instantiate them with tactics?

Robin Arnez (Jun 20 2025 at 06:40):

When you create meta-variables using ?name, they are synthetic-opaque, i.e. defeq will never assign them. Non synthetic-opaque meta-variables are created by _ but that won't work here, the constructor tactic also creates non-synthetic-opaque meta-variables though so that's what you can use.

Jason Gross (Jun 20 2025 at 06:41):

Thanks! Why doesn't _ work?

Robin Arnez (Jun 20 2025 at 12:02):

Because refine expects all non-synthetic-opaque metavariables to be assigned

Robin Arnez (Jun 20 2025 at 12:03):

apply doesn't have that restriction but also doesn't use the goal type so you need:

def construct_by_refinement :  (result : Nat), result = 5 := by
  apply (⟨_, ?pf :  (result : Nat), result = 5)
  case pf => rfl

Adam Topaz (Jun 20 2025 at 13:07):

This apply is not ideal IMO. But the version I came up with is also not ideal:

def construct_by_refinement :  (result : Nat), result = 5 := by
  run_tac do Meta.mkFreshExprMVar (userName := `result) none
  refine ⟨?result, ?_⟩
  rfl

I hope someone else can chime in with the proper way to do this.

Dan Velleman (Jun 20 2025 at 15:25):

This seems to work:

def construct_by_refinement :  (result : Nat), result = 5 := by
  apply Exists.intro
  rfl

Robin Arnez (Jun 20 2025 at 15:27):

Yeah, right, constructor, or apply Exists.intro works. I just wanted to demonstrate how refine and apply differ in handling metavariables

Adam Topaz (Jun 20 2025 at 15:31):

Doing something like apply ConstructorName or constructor is also not ideal, because you may want to explicitly say which parameters of the constructor you want to specify ahead of time, and which should be determined later after some unification. So IMO there should be some way to do it with refine (or at least with refine-like syntax)

Robin Arnez (Jun 20 2025 at 15:38):

Hmm at that point you could probably just specify them before though; having metavariables float around is less than ideal -- I even thought at some point that that was a bug in apply (putting the goal for the proof first) and decided to use refine instead for this reason (I've since understood it more but still)

Robin Arnez (Jun 20 2025 at 15:38):

also, apply Exists.intro _ ?prf?

Kyle Miller (Jun 20 2025 at 16:13):

Here's a workaround:

def construct_by_refinement :  (result : Nat), result = 5 := by
  refine ⟨?result, ?pf
  case pf => refine rfl

The refine tactic turns on a feature to assign synthetic opaque metavariables. (No one is expected to know this. Consider this to be a hack.) By the way, apply's current behavior is more-or-less a bug, so that's a hack too.

This is a usability problem that comes up frequently with existentials. Sometimes you want to let the value follow from the proof rather than vice versa.

I think it's possible that at some point we might have "synthetic semiopaque metavariables", and either _ would work here:

def construct_by_refinement :  (result : Nat), result = 5 := by
  refine ⟨_, ?pf
  case pf => rfl

or you'd write ??, or some other sigil:

def construct_by_refinement :  (result : Nat), result = 5 := by
  refine ⟨??result, ?pf
  case pf => rfl

This "synthetic opaque" concept is to help address issues where goals would go missing in counterintuitive ways, plus they need to be handled differently internally to make sure that they remain assignable in tactics. The design is also meant to prevent people from writing "swiss cheese proofs", with holes in all the types, since they tend to be less maintainable.

Something that would help get semiopaque metavariables into Lean is a collection of case studies demonstrating it would be a useful proof organization device.

Adam Topaz (Jun 20 2025 at 22:43):

Would it make sense to have something like the following for now?

import Mathlib.Tactic
import Batteries.Lean.Meta.Basic

open Lean Elab

elab "unifying" id:ident tacs:tacticSeq : tactic => commitIfNoEx do
    let some (mvarid, decl) := ( getMCtx).decls.toArray.find? fun (_, decl) =>
        decl.userName == id.getId
      | throwError "Couldn't find mvar named {id}"
    modifyMCtx fun mctx => mctx.setMVarKind mvarid .natural
    Tactic.evalTacticSeq tacs
    modifyMCtx fun mctx => mctx.setMVarKind mvarid decl.kind

def construct_by_refinement :  (result : Nat), result = 5 := by
  refine ⟨?result, ?pf
  unifying result
    case pf => rfl

Last updated: Dec 20 2025 at 21:32 UTC