Zulip Chat Archive

Stream: Is there code for X?

Topic: pretty print of Nat


Asei Inoue (Apr 21 2024 at 14:30):

Is there a way to replace the combination of Nat.zero and Nat.succ appearing in infoview with "0, 1, 2, 3, ..." ?

import Mathlib.Tactic

/-
![1, 2, 3] : Fin (Nat.succ (Nat.succ (Nat.succ 0))) → ℕ
-/
#check ![1, 2, 3]

Kyle Miller (Apr 21 2024 at 18:26):

Related question: https://leanprover.zulipchat.com/#narrow/stream/287929-mathlib4/topic/.E2.9C.94.20More.20convenient.20notation.20for.20.23check.20a.20FinVec/near/434055374

The answer is that there's no pretty printer option for this. It's possible for you to write your own delaborator to pretty print these expressions in the way you want though.

Asei Inoue (Apr 22 2024 at 09:22):

Thank you!

Asei Inoue (Apr 22 2024 at 09:24):

I will learn Lean more to understand this fantastic language…

Filippo A. E. Nuccio (Mar 02 2025 at 17:10):

While doing some exercises from the "Metaprogramming in Lean" book, I clicked on the solutions and got the following code

import Mathlib
import Init.Prelude

open Lean Lean.Expr Lean.Meta

elab "explore" : tactic => do
  let mvarId : MVarId  Lean.Elab.Tactic.getMainGoal
  let metavarDecl : MetavarDecl  mvarId.getDecl

  IO.println "Our metavariable"
  -- [anonymous] : 2 = 2
  IO.println s!"\n{metavarDecl.userName} : {metavarDecl.type}"

  IO.println "\nAll of its local declarations"
  let localContext : LocalContext := metavarDecl.lctx
  for (localDecl : LocalDecl) in localContext do
    if localDecl.isImplementationDetail then
      -- (implementation detail) red : 1 = 1 → 2 = 2 → 2 = 2
      IO.println s!"\n(implementation detail) {localDecl.userName} : {localDecl.type}"
    else
      -- hA : 1 = 1
      -- hB : 2 = 2
      IO.println s!"\n{localDecl.userName} : {localDecl.type}"

theorem red (hA : 1 = 1) (hB : 2 = 2) : 2 = 2 := by
  explore
  sorry

but in the infoview I have a bunch of (OfNat.ofNat.{0} Nat 1 (instOfNatNat 1)) (OfNat.ofNat.{0} Nat 1 (instOfNatNat 1))) instead of 1. Is there a pretty printer to call so that this gets bettered rendered?

Aaron Liu (Mar 02 2025 at 17:17):

Use docs#Lean.logInfo instead:

import Lean

open Lean Lean.Expr Lean.Meta

elab "explore" : tactic => do
  let mvarId : MVarId  Lean.Elab.Tactic.getMainGoal
  let metavarDecl : MetavarDecl  mvarId.getDecl

  logInfo "Our metavariable"
  -- hA : 1 = 1
  -- hB : 2 = 2
  -- ⊢ 2 = 2
  logInfo mvarId

  logInfo "All of its local declarations"
  let localContext : LocalContext := metavarDecl.lctx
  for (localDecl : LocalDecl) in localContext do
    if localDecl.isImplementationDetail then
      -- (implementation detail) red : 1 = 1 → 2 = 2 → 2 = 2
      logInfo m!"(implementation detail) {localDecl.userName} : {localDecl.type}"
    else
      -- hA : 1 = 1
      -- hB : 2 = 2
      logInfo m!"{localDecl.userName} : {localDecl.type}"

theorem red (hA : 1 = 1) (hB : 2 = 2) : 2 = 2 := by
  explore
  sorry

Filippo A. E. Nuccio (Mar 02 2025 at 17:22):

Thanks!

Filippo A. E. Nuccio (Mar 04 2025 at 17:23):

I realise that this is still not exactly what I needed: for pedagogical purposes I am defining a tactic that add the double of any natural in scope:

import Lean.Elab
import Mathlib.Tactic

open Lean Elab Tactic

def SuccNat : TacticM Unit :=
  withMainContext do
  let lctx  getLCtx
  for h in lctx do
  if h.type == .const ``Nat [] then
    liftMetaTactic fun mv => do
      let mv  mv.define s!"double_{h.userName.toString}".toName (.const ``Nat [])
        (Lean.mkAppN (.const ``Nat.mul []) #[mkNatLit 2, h.toExpr])
      let (_, mv)  mv.intro1P
      return [mv]

elab "succNat" : tactic => SuccNat

but when I do

example (n m k : ) (H : n = 3 + 1) : True := by
  succNat
  sorry

I have in my infoview

double_n :  := Nat.mul 2 n
double_m :  := Nat.mul 2 m
double_k :  := Nat.mul 2 k

whereas I would like them rendered as 2 * n, 2 * m, etc...

Damiano Testa (Mar 04 2025 at 17:38):

Here is one way:

def SuccNat : TacticM Unit := withMainContext do
  let lctx  getLCtx
  for h in lctx do
    if h.type == .const ``Nat [] then
      let hval := h.toExpr
      let hstx  Expr.toSyntax hval
      let tm  `(term| 2 * $hstx)
      let tme  elabTerm tm h.type
      liftMetaTactic fun mv => do
        let mv  mv.define s!"double_{h.userName.toString}".toName h.type tme
        let (_, mv)  mv.intro1P
        return [mv]

Filippo A. E. Nuccio (Mar 04 2025 at 17:42):

Nice! Can you explain what these lines

    let tm  `(term| 2 * $hstx)
    let tme  elabTerm tm (some h.type)

are (in paticular the term| and the elabTerm) and why this works (or, equivalently, why the previous did not)?

Damiano Testa (Mar 04 2025 at 17:43):

The first one, is generating a Syntax object (in fact, a Term) that represents 2 * <the other>.

Damiano Testa (Mar 04 2025 at 17:44):

Now that you have a Syntax, you can elaborate it and produce an expression: this is what the other line does.

Filippo A. E. Nuccio (Mar 04 2025 at 17:44):

Oh, so I was roughly working at the Expr level, and you are somewhat deeper?

Damiano Testa (Mar 04 2025 at 17:44):

The main takeaway is that Lean is generating the expr for you, so you do not have to worry about what mkApp you should use.

Filippo A. E. Nuccio (Mar 04 2025 at 17:45):

(Apologies for the dummy question, I am moving my first steps in this)

Damiano Testa (Mar 04 2025 at 17:45):

I would actually say that my code is more shallow.

Damiano Testa (Mar 04 2025 at 17:46):

I view "depth" as follows:

  • text that you type -- very much on the surface;
  • Lean converts it to syntax -- a little bit deeper;
  • eventually, from the syntax lean produces an expr -- deep here!

Filippo A. E. Nuccio (Mar 04 2025 at 17:47):

Oh, I got syntax <-> expr in the wrong order. Thanks!

Damiano Testa (Mar 04 2025 at 17:47):

Right: I mostly view Syntax as a step towards producing an Expr, rather than the other way round.

Filippo A. E. Nuccio (Mar 04 2025 at 17:47):

So, is it roughly correct that you try to work with syntax as much as possible and revert to Expr only when needed?

Filippo A. E. Nuccio (Mar 04 2025 at 17:48):

(I am a bit confused because studying the Metaprogramming book, I feel more emphasis on Expr than on Stx)

Damiano Testa (Mar 04 2025 at 17:48):

I am not sure if that is always the case: eventually, you want the Expr to line up, but if you can make Lean do the conversion/elaboration, that is more stable than doing it yourself.

Filippo A. E. Nuccio (Mar 04 2025 at 17:48):

I see

Damiano Testa (Mar 04 2025 at 17:49):

I would view writing directly the Expr as "unfolding all the way".

Damiano Testa (Mar 04 2025 at 17:49):

Sometimes, there is no way around it and ultimately, you should be able to do things like that.

Damiano Testa (Mar 04 2025 at 17:49):

In practice, though, if you can leverage some intermediate representations that takes care of the conversion for you, you are better off using them.

Filippo A. E. Nuccio (Mar 04 2025 at 17:50):

OK, very nice. But this means in particular that the infoview is really affected by the way I produce things, and does nothing "on its own" like translating expressions back and forth when notation is in force?

Damiano Testa (Mar 04 2025 at 17:51):

I would say that pretty-printing is very susceptible of what exactly you use. This is why, if you have syntax for something, you should use it, since otherwise it will "mean the same", but nothing will pick it up.

Damiano Testa (Mar 04 2025 at 17:52):

In the case above, you want Lean to latch onto whatever it is that uses the * notation.

Damiano Testa (Mar 04 2025 at 17:52):

I think that this is the HMul typeclass, that, for Nat, is highjacked... maybe the Nat.mul that you were using, I am not sure.

Filippo A. E. Nuccio (Mar 04 2025 at 17:53):

I see, I certainly need to get more experience, but little by little it will improve. I had indeed tried with the HMul, but at the Expr-level it is a nightmare, because it needs to be filled with all the arguments (the three types) and with the HMul instance, itselft having several arguments (and coming from Mul for Nat, that again needs arguments...)

Aaron Liu (Mar 04 2025 at 17:54):

If you turn off notation, 2 * n is printed as HMul.hMul 2 n

Filippo A. E. Nuccio (Mar 04 2025 at 17:54):

Aaron Liu said:

If you turn off notation, 2 * n is printed as HMul.hMul 2 n

But my point was to turn it on

Damiano Testa (Mar 04 2025 at 17:55):

Right, if you use

        ( Meta.mkAppM ``HMul.hMul #[mkNatLit 2, h.toExpr])

in the appropriate line of your code, you will see that it "works" as is.

Damiano Testa (Mar 04 2025 at 17:56):

Filippo A. E. Nuccio said:

I see, I certainly need to get more experience, but little by little it will improve. I had indeed tried with the HMul, but at the Expr-level it is a nightmare, because it needs to be filled with all the arguments (the three types) and with the HMul instance, itselft having several arguments (and coming from Mul for Nat, that again needs arguments...)

Note that I used the helper Meta.mkAppM that have a better UX.

Filippo A. E. Nuccio (Mar 04 2025 at 17:56):

Yes, because mkAppM is more clever wrt implicit arguments, right?

Aaron Liu (Mar 04 2025 at 17:56):

Nat.mul does not have notation, and is not the same as @HMul.hMul Nat Nat Nat (@instHMul Nat instMulNat)

Damiano Testa (Mar 04 2025 at 17:56):

Using Lean.mkAppN directly is really hardcore (and really brittle).

Aaron Liu (Mar 04 2025 at 17:57):

For this I would use docs#Lean.mkNatMul, since it is specialized to this use case.

Damiano Testa (Mar 04 2025 at 17:57):

So, `(syntax) and Meta.mkAppN are more user-friendly than constructing the inductive Syntax and Expr directly.

Aaron Liu (Mar 04 2025 at 17:57):

Usually

Damiano Testa (Mar 04 2025 at 17:58):

In general, you should always prefer them over anything else and use the constructors only as a last resort.

Damiano Testa (Mar 04 2025 at 17:59):

Ah, if there is a dedicated function, then great!

Filippo A. E. Nuccio (Mar 04 2025 at 17:59):

Aaron Liu said:

For this I would use docs#Lean.mkNatMul, since it is specialized to this use case.

And this brings me to the ancillary question... how to survive the enormous zoo of functions around? I understand it is probably the same question one could ask for Mathlib, but for me the Lean.Elab zoo seems much wilder :lion: :tiger: I mean, should I hope to eventually learn everything that is in there? Are there guide(line)s? General principles to browse the library?

Filippo A. E. Nuccio (Mar 04 2025 at 18:04):

At any rate, thank you both for your answers ! :pray:

Damiano Testa (Mar 04 2025 at 18:04):

I think that yes, it is like with mathlib: you ask questions, you browse a little...

Damiano Testa (Mar 04 2025 at 18:05):

In the case of metaprogramming, "proofs", i.e. reading after the :=, is probably more informative than doing it in mathlib.

Kyle Miller (Mar 04 2025 at 19:28):

You could also take more advantage of evalTactic here. It feels less robust in some ways, but the positive to the tradeoff is that the code is a lot simpler to understand:

def SuccNat : TacticM Unit := withoutRecover <| withMainContext do
  let lctx  getLCtx
  for h in lctx do
    if h.type.isConstOf ``Nat then
      let doubleIdent := mkIdent <| Name.mkSimple s!"double_{h.userName.toString}"
      let hstx  exprToSyntax h.toExpr
      evalTactic <|  `(tactic| let $doubleIdent : Nat := 2 * $hstx)

elab "double_nats" : tactic => SuccNat

example (a b c : Nat) (d : Int) : True := by
  double_nats
/-
a b c : Nat
d : Int
double_a : Nat := 2 * a
double_b : Nat := 2 * b
double_c : Nat := 2 * c
⊢ True
-/

Filippo A. E. Nuccio (Mar 04 2025 at 19:29):

Is evalTactic explained somewhere (perhaps in the Metaprogramming book, or elsewhere)?

Kyle Miller (Mar 04 2025 at 19:32):

I don't know whether it is or not, but it ought to be. It's one of the easiest ways to manipulate the tactic state, since it lets you run tactic scripts directly. It lets you make macros that depend on the details of the current state (here, we can't use a macro because we need the current local context).

Filippo A. E. Nuccio (Mar 04 2025 at 19:34):

Ok, thanks, I'll try to study/understand it and how it works.

Kyle Miller (Mar 04 2025 at 19:42):

I made a small modification: the additional withoutRecover makes sure the tactic in evalTactic is being run in a mode where it doesn't try to do any error recovery. For example, inserting sorry when there are type errors.

In the automated parts of a tactic, you want recovery disabled, since you expect them to be error-free. If anything goes wrong, you want the tactic to stop with an error so you can debug it.

(You can experiment with the effects of withoutRecover by changing the evalTactic line to do something bogus, like let $doubleIdent : Nat := true * $hstx)

Kyle Miller (Mar 04 2025 at 19:50):

I think the fact it was easy to forget to include withoutRecover proves the thesis that the Lean 4 metaprogramming interface is very large and difficult to keep track of.

I try to keep Metaprogramming gotchas updated. Just added withoutRecover at the end.

Filippo A. E. Nuccio (Mar 04 2025 at 19:51):

Thanks! If you feel like adding a paragraph about evalTactic (or more about EvalM in general... :smile: ) it would be very much appreciated!

Damiano Testa (Mar 04 2025 at 19:54):

Also, for pedagogical purposes, I find the exact checking that the type of your local declarations is Nat very strong. If this were a "real" tactic, I think that it would probably try a little harder to unify h.type with Nat, probably using Meta.whnfR.

Damiano Testa (Mar 04 2025 at 19:55):

E.g., if you had abbrev N := Nat and then you had an s : N in your context, it may not be unreasonable to want to double s as well.

Kyle Miller (Mar 04 2025 at 19:55):

In fact, that points to how the tactic is afflicted by one of the gotchas :-)

example (a b c : Nat) (d : Int) : True := by
  let e := 2
  double_nats
  -- e is not doubled

Kyle Miller (Mar 04 2025 at 19:55):

Quiz: which gotcha is it?

Damiano Testa (Mar 04 2025 at 19:59):

This is one of the most common issues!! :smile:

Filippo A. E. Nuccio (Mar 04 2025 at 20:00):

Kyle Miller said:

Quiz: which gotcha is it?

Oh nice! I'll try to do the exercise tonight :smirk: Thanks.

Damiano Testa (Mar 04 2025 at 20:01):

(And I still consider this kind of "action at a distance" very confusing, btw.)

Filippo A. E. Nuccio (Mar 04 2025 at 20:03):

Filippo A. E. Nuccio said:

Kyle Miller said:

Quiz: which gotcha is it?

Oh nice! I'll try to do the exercise tonight :smirk: Thanks.

Is it in some sense thinking that e must be an integer (although it shows as a Nat in the infoview) perhaps?

Damiano Testa (Mar 04 2025 at 20:04):

I think that even without d, it would still not work.

Damiano Testa (Mar 04 2025 at 20:05):

But I also consider the d a hint for figuring out what is going on.

Kyle Miller (Mar 04 2025 at 20:06):

I'm not sure d is related at all. It's not a metadata bug, if that's what you're thinking. (E.g., one solved by consumeMData on the goal type.)

Damiano Testa (Mar 04 2025 at 20:07):

No, I was not thinking of a metadata issue (although it is the first thing that I thought when I saw your example).

Damiano Testa (Mar 04 2025 at 20:08):

Aaron Liu (Mar 04 2025 at 20:36):

Kyle Miller said:

Quiz: which gotcha is it?

Is it

spoiler

Filippo A. E. Nuccio (Mar 05 2025 at 10:05):

Aaron Liu said:

Kyle Miller said:

Quiz: which gotcha is it?

Is it

spoiler


Oh, and why so!?! Is the let clause creating a mvar for the type of e or something?

Kyle Miller (Mar 05 2025 at 10:53):

let e := 2 is basically shorthand for let e : _ := 2, and so, yes, the type of e is a metavariable. The metavariable is assigned very soon after it is created, but it's still there.

One fix is to do

def SuccNat : TacticM Unit := withoutRecover <| withMainContext do
  let lctx  getLCtx
  for h in lctx do
    let htype  instantiateMVars h.type
    if htype.isConstOf ``Nat then
      let doubleIdent := mkIdent <| Name.mkSimple s!"double_{h.userName.toString}"
      let hstx  exprToSyntax h.toExpr
      evalTactic <|  `(tactic| let $doubleIdent : Nat := 2 * $hstx)

Another is to put it into whnf as Damiano suggested:

def SuccNat : TacticM Unit := withoutRecover <| withMainContext do
  let lctx  getLCtx
  for h in lctx do
    if ( whnfR h.type).isConstOf ``Nat then
      let doubleIdent := mkIdent <| Name.mkSimple s!"double_{h.userName.toString}"
      let hstx  exprToSyntax h.toExpr
      evalTactic <|  `(tactic| let $doubleIdent : Nat := 2 * $hstx)

Filippo A. E. Nuccio (Mar 05 2025 at 11:04):

While I understand why the first solution works, I do not for Damiano's. Is whnfR instantiating mvars under the hood? I would have imagined it only to be able to fix problems related to Nat being "locally presented" not as .const ``Nat [], not to be able to manage this kind of uninstantiated mvars.

Kyle Miller (Mar 05 2025 at 11:10):

Yes, part of the definition of weak-head normal form is that there are no head metavariables that have been assigned. That is, if you have ?m a1 a2 ... an with ?m an assigned metavariable, it will instantiate it.

Filippo A. E. Nuccio (Mar 05 2025 at 11:11):

Awesome! Thanks (this probably is also worth mentioning in the relevant chapter, no?)

Kyle Miller (Mar 05 2025 at 11:17):

It looks like they briefly mention it, a bit indirectly:

?x 0 1  -- Assuming the metavariable `?x` is unassigned, it is in WHNF.

?x 0 1 -- Assuming `?x` is assigned (e.g. to `Nat.add`), its application is not
          in WHNF.

Damiano Testa (Mar 05 2025 at 14:21):

In terms of "standard tactic expectation", I would say that mathlib tactics would normally expect using whnfR for normalization, than just instantiating metavariables.


Last updated: May 02 2025 at 03:31 UTC