Zulip Chat Archive
Stream: lean4
Topic: Qq and reflecting meta-level values
Alex Keizer (Apr 15 2023 at 16:15):
I'm using Qq and I want to turn some i : Foo
into a i : Q(Foo)
, so that I can use it in subsequent q
/Q
macros. I noticed that for n : Nat
this just works, but for other types I get an unknown identifier
error.
import Qq
open Qq
example (n : Nat) : Q(Nat) :=
q($n)
example (i : Fin 2) : Q(Fin 2) :=
q($i)
-- ^------ unknown identifier '«$i»'
Is Nat
just hard-coded, or is there some typeclass I can implement to make this work?
Eric Wieser (Apr 15 2023 at 16:59):
Does docs4#Fin have a docs4#Lean.ToExpr instance?
Eric Wieser (Apr 15 2023 at 17:04):
In lean 3 it was docs#fin.reflect
Eric Wieser (Apr 15 2023 at 17:05):
I would guess we just dropped it when porting
Alex Keizer (Apr 15 2023 at 17:51):
Yeah, ToExpr is the typeclass I was looking for, after implementing it for Fin
the error disappears
Alex Keizer (Apr 15 2023 at 17:54):
Is there a reason ToExpr
takes Type u
rather than Sort u
? I implemented ToExpr (Fin n)
by translating proofs of x < y
to Expr
, so I figured I could define ToExpr (x < y)
as well, but that of course doesn't work if ToExpr
only takes types.
import Qq
open Qq
open Lean (ToExpr)
def Nat.lt.toExpr : {x y : Nat} → (x < y) → Q($x < $y)
| 0, 0, h => by contradiction
| 0, y+1, _ => q(Nat.zero_lt_succ $y)
| x+1, y+1, h =>
have h : x < y := Nat.lt_of_succ_lt_succ h
let r : Q($x < $y) := toExpr h
q(Nat.succ_lt_succ $r)
instance {n : Nat} : ToExpr (Fin n) where
toExpr := fun ⟨i, h⟩ => q(Fin.mk $i $(Nat.lt.toExpr h))
toTypeExpr := q(Fin $n)
Kyle Miller (Apr 15 2023 at 17:56):
Looks like we created effectively the same implementation in parallel:
open Lean in
/-- Returns a proof of a < b -/
def ltToExpr (a b : Nat) (h : a < b) : Expr :=
match a, b with
| _, 0 => (Nat.not_lt_zero _ h).elim
| 0, b + 1 => .app (.const ``Nat.succ_pos []) (toExpr b)
| a + 1, b + 1 =>
let e := ltToExpr a b (Nat.lt_of_succ_lt_succ h)
mkApp3 (.const ``Nat.succ_lt_succ []) (toExpr a) (toExpr b) e
open Lean in
instance : ToExpr (Fin n) where
toExpr x := mkApp3 (.const ``Fin.mk []) (toExpr n) (toExpr x.1) (ltToExpr _ _ x.2)
toTypeExpr := .app (.const ``Fin []) (toExpr n)
set_option pp.proofs true
#eval Lean.PrettyPrinter.ppExpr <| Lean.toExpr (3 : Fin 5)
-- { val := 3, isLt := Nat.succ_lt_succ (Nat.succ_lt_succ (Nat.succ_lt_succ (Nat.succ_pos 1))) }
Kyle Miller (Apr 15 2023 at 18:08):
Maybe ToExpr
takes a Type _
because there's no runtime values associated to proofs (they're erased)?
We could use PLift
here to create these sorts of instances anyway, at the cost of needing to insert some PLift.up
s and PLift.down
s:
import Qq
open Qq
open Lean
def Nat.lt.toExpr : {x y : Nat} → (x < y) → Q($x < $y)
| _, 0, h => (Nat.not_lt_zero _ h).elim
| 0, y+1, _ => q(Nat.zero_lt_succ $y)
| x+1, y+1, h =>
let r : Q($x < $y) := toExpr (Nat.lt_of_succ_lt_succ h)
q(Nat.succ_lt_succ $r)
instance {x y : Nat} : ToExpr (PLift (x < y)) where
toExpr h := q(PLift.up $(Nat.lt.toExpr h.down))
toTypeExpr := q(PLift ($x < $y))
instance {n : Nat} : ToExpr (Fin n) where
toExpr := fun ⟨i, h⟩ =>
let h' : Q(PLift ($i < $n)) := toExpr <| PLift.up h
q(Fin.mk $i (PLift.down $h'))
toTypeExpr := q(Fin $n)
Arthur Adjedj (Apr 15 2023 at 18:14):
Another possible reason might be that most terms in Prop
can’t be eliminated to Type _
, making ToExpr
only useful for subsingleton types, for which the rendered expressions would be trivial anyway
Kyle Miller (Apr 15 2023 at 18:23):
I don't think that's explaining it -- all ToExpr p
needs to do is synthesize a proof of p
using the symbolic structure of p
itself, given a proof of p
. Look at how Nat.lt.toExpr
is defined. It recurses on x
and y
, and then it only uses h
when it's equivalent to False
to be able to omit a case.
The situation seems to be similar to Decidable
instances. In fact, one could imagine a typeclass DecideExpr p
that synthesizes an expression for either a proof of p
or a proof of not p
, depending on which is true. Then given one of these you can just use that proof directly for a ToExpr
instance (since you know ahead of time the DecideExpr
instance will never produce the negation proof).
Edit: I suppose this is all to say that you're right that it's important that propositions are subsingletons, with the caveat that since typeclass inference can look at the symbolic structure of the proposition there's still a lot you can do.
Alex Keizer (Apr 15 2023 at 18:24):
Kyle Miller said:
Maybe
ToExpr
takes aType _
because there's no runtime values associated to proofs (they're erased)?
That's true, but Expr
isn't quite a runtime value yet. We do want to be able to construct expressions that correspond to proofs of Prop
s (as witnessed by our implementations of ToExpr (Fin n)
). The PLift
trick is a cool workaround (thanks for showing it!), but for me it just reaffirms that it would be useful to have some built-in way to reflect a Prop
.
Another possible reason might be that most terms in Prop can’t be eliminated to Type _, making ToExpr only useful for subsingleton types, for which the rendered expressions would be trivial anyway
That's fair, the translation cannot inspect the actual meta-level proof object, so maybe ToExpr
is not the right class. Effectively, we just want to know that Q(p)
is inhabited, for some p : Prop
.
Possibly a solution is to special case Qq so that q($h)
where the type of h
is a P : Prop
instead tries to synthesize Inhabited Q($P)
Kyle Miller (Apr 15 2023 at 18:28):
Alex Keizer said:
That's true, but
Expr
isn't quite a runtime value yet.
I agree (and the PLift
trick shows these sorts of instances aren't useless). What I meant is that I believe the intended use was to be able to #eval
an expression and then toExpr
it. We're sort of misusing it here (not to say that we shouldn't be allowed to misuse it!) because we're using the fact that typeclass inference is symbolic, so from the elaboration-time type we can get x
and y
.
Eric Wieser (Apr 15 2023 at 18:31):
Shouldn't Fin.toExpr
generate ofNat
like it did in Lean 3?
Eric Wieser (Apr 15 2023 at 18:32):
I thought the idea was that it generate the canonical expression, as opposed to the most efficient one
Alex Keizer (Apr 16 2023 at 10:45):
I mean, I just learned about ToExpr
yesterday, so I'm maybe not the most qualified person on that subject, but it seems that for Qq
's purposes it's beneficial to generate more efficient representations if we can. Compile-times are already not great, so if we can cut that down a bit, that seems good. (If there are other issues that I'm overlooking, please do tell me).
Now, since Kyle mentioned that maybe we're misusing ToExpr
, I figured I could try making a dedicated QuoteExpr
typeclass that does allow for Prop
s. It also uses the QQ
type-family for some extra type-safety (at the cost of flexibility with the universe)
/--
Gives a meta-level representation of the universe in which some type `α` lives
-/
class QuoteUnivOf (α : Sort u) where
/-- Object representing the universe of `α` -/
quoteUniv : Level
/--
Converts a value of type `α` into an expression that represents this value in Lean.
This class differs from the built-in `ToExpr` in two ways:
* It allows implementation for `α : Prop`, whereas `ToExpr` is only for `Type _`s
* We use the `QQ` type family to assert the type of the returned expressions.
This ensures this type information is available when implementors use the `q(⋅)` macro
to construct the expression.
-/
class QuoteExpr (α : Sort u) extends QuoteUnivOf α where
/-- Expression representing the type `α` -/
quoteTypeExpr : QQ (Expr.sort quoteUniv)
/-- Convert a value `a : α` into an expression that denotes `a` -/
quoteExpr : α → QQ quoteTypeExpr
With this class, we can write the following
import Qq.QuoteExpr
import Qq.Macro
open Lean (Level ToExpr)
namespace Qq
instance (α : Sort 0) : QuoteUnivOf α := ⟨Level.ofNat 0⟩
instance (α : Sort 1) : QuoteUnivOf α := ⟨Level.ofNat 1⟩
/--
Define a fallback instance of `QuoteExpr` for implementors of `ToExpr`
-/
instance (priority := low) {α : Type u} [ToExpr α] [QuoteUnivOf α] : QuoteExpr α where
quoteTypeExpr := ToExpr.toTypeExpr α
quoteExpr a := ToExpr.toExpr a
instance (a b : Nat) : QuoteExpr (a < b) where
quoteTypeExpr := q($a < $b)
quoteExpr := go where
go {a b : Nat} (h : a < b) : Q($a < $b) := match a, b with
| 0, 0 => by contradiction
| 0, b+1 => q(Nat.zero_lt_succ $b)
| a+1, b+1 =>
have h := Nat.lt_of_succ_lt_succ h
let h := go h
q(@Nat.succ_lt_succ $a $b $h)
instance (n : Nat) : QuoteExpr (Fin n) where
quoteTypeExpr := q(Fin $n)
quoteExpr := fun ⟨i, h⟩ => q(⟨$i, $h⟩)
end Qq
Alex Keizer (Apr 16 2023 at 10:53):
Re: the universe flexibility. I think we do lose the flexibility to define universe polymorphic instances, since we have provide a Level
object .
Is it possible to define some universe polymorphic quoteLevel.{u} : Level
so that
quoteLevel.{0} = Level.ofNat 0
quoteLevel.{1} = Level.ofNat 1
quoteLevel.{2} = Level.ofNat 2
...
I would expect not.
Then again, I also wonder whether it's even possible to write a correct universe polymorphic ToExpr
instance. Presumably, if the constructed expression indeed lives in some arbitrary universe u
, the expressions contains a Level
representation of this universe anyway.
Eric Wieser (Apr 16 2023 at 11:02):
We already have this, its docs4#ToLevel
Alex Keizer (Apr 16 2023 at 11:04):
Hmm, that link 404's
Alex Keizer (Apr 16 2023 at 11:13):
I found it at https://github.com/leanprover-community/mathlib4/blob/94c39a2c516923d789c4061d221b746708eed5dd/Mathlib/Tactic/ToLevel.lean
That one is indeed better than my attempt, thanks!
What I meant, though, is some definition of quoteLevel
where even
universe u
quoteLevel.{u}
Would do the right thing, I expect this to be impossible
Eric Wieser (Apr 16 2023 at 11:25):
What would be the point?
Eric Wieser (Apr 16 2023 at 11:26):
Just write [ToLevel.{u}]
as a hypothesis instead
Eric Wieser (Apr 16 2023 at 11:26):
In all the cases where you actually need it I think Lean will find the instance
Alex Keizer (Apr 16 2023 at 11:36):
Yeah, I also think ToLevel
is enough, I was just wondering if I was overlooking something.
My definition of QuoteExpr \alpha
(where \alpha : Sort u
) indeed depends on a ToLevel.{u}
instance being found, whereas it is possible to define ToExpr \alpha
without it. Hence, in theory, you lose some flexibility. In practice, I expect actually defining a correct ToExpr
instance is impossible without ToLevel.{u}
. Unless something like quoteLevel
as I described it were possible
Alex Keizer (Apr 17 2023 at 19:46):
Alex Keizer said:
I mean, I just learned about
ToExpr
yesterday, so I'm maybe not the most qualified person on that subject, but it seems that forNow, since Kyle mentioned that maybe we're misusing
ToExpr
, I figured I could try making a dedicatedQuoteExpr
typeclass that does allow forProp
s. It also uses the/-- Gives a meta-level representation of the universe in which some type `α` lives -/ class QuoteUnivOf (α : Sort u) where /-- Object representing the universe of `α` -/ quoteUniv : Level /-- Converts a value of type `α` into an expression that represents this value in Lean. This class differs from the built-in `ToExpr` in two ways: * It allows implementation for `α : Prop`, whereas `ToExpr` is only for `Type _`s * We use the `QQ` type family to assert the type of the returned expressions. This ensures this type information is available when implementors use the `q(⋅)` macro to construct the expression. -/ class QuoteExpr (α : Sort u) extends QuoteUnivOf α where /-- Expression representing the type `α` -/ quoteTypeExpr : QQ (Expr.sort quoteUniv) /-- Convert a value `a : α` into an expression that denotes `a` -/ quoteExpr : α → QQ quoteTypeExpr
With this class, we can write the following
import Qq.QuoteExpr import Qq.Macro open Lean (Level ToExpr) namespace Qq instance (α : Sort 0) : QuoteUnivOf α := ⟨Level.ofNat 0⟩ instance (α : Sort 1) : QuoteUnivOf α := ⟨Level.ofNat 1⟩ /-- Define a fallback instance of `QuoteExpr` for implementors of `ToExpr` -/ instance (priority := low) {α : Type u} [ToExpr α] [QuoteUnivOf α] : QuoteExpr α where quoteTypeExpr := ToExpr.toTypeExpr α quoteExpr a := ToExpr.toExpr a instance (a b : Nat) : QuoteExpr (a < b) where quoteTypeExpr := q($a < $b) quoteExpr := go where go {a b : Nat} (h : a < b) : Q($a < $b) := match a, b with | 0, 0 => by contradiction | 0, b+1 => q(Nat.zero_lt_succ $b) | a+1, b+1 => have h := Nat.lt_of_succ_lt_succ h let h := go h q(@Nat.succ_lt_succ $a $b $h) instance (n : Nat) : QuoteExpr (Fin n) where quoteTypeExpr := q(Fin $n) quoteExpr := fun ⟨i, h⟩ => q(⟨$i, $h⟩) end Qq
@Gabriel Ebner would you be interested in a PR to Qq
with this typeclass? Even if ToExpr
gets changed to also accept Prop
s, having a Qq
specific typeclass (with instances going back and forth) has the benefit of propagating expected types a bit better, if implementors use q(...)
to construct their expression. Of course, it has the downside of being a bit confusing, having ToExpr
and QuoteExpr
for basically the same purpose.
Kyle Miller (Apr 17 2023 at 20:25):
Here's another design:
import Mathlib.Tactic
import Mathlib.Tactic.ToExpr
import Qq
open Lean Qq
class ReflectType (α : Sort u) [ToLevel.{u}] where
reflectType : QQ (Expr.sort toLevel.{u})
export ReflectType (reflectType)
class Reflect (α : Sort u) [ToLevel.{u}] [ReflectType α] where
reflect : α → QQ (reflectType α)
export Reflect (reflect)
Kyle Miller (Apr 17 2023 at 20:25):
This lets you reflect types even if their terms aren't in general reflectable. For example, function types:
instance (α : Type u) (β : Type v) [ToLevel.{u+1}] [ToLevel.{v+1}] [ReflectType α] [ReflectType β]
[ToLevel.{max (u+1) (v+1)}] : ReflectType (α → β) where
reflectType := Expr.forallE `x (reflectType α) (reflectType β) .default
Kyle Miller (Apr 17 2023 at 20:26):
It's also easy to use pre-existing ToExpr
instances to define these instances:
instance : ReflectType Nat where
reflectType := toTypeExpr Nat
instance : Reflect Nat where
reflect := toExpr
Kyle Miller (Apr 17 2023 at 20:36):
I don't think I've got things quite right yet; universe variables don't seem to be handled properly. Ideally that function instance could be written using q(...)
somehow.
Eric Wieser (Apr 17 2023 at 20:41):
Eric Wieser said:
Shouldn't
Fin.toExpr
generateofNat
like it did in Lean 3?
I've attempted to PR this in !4#3486, though I can't work out how to use =Q
Eric Wieser (Apr 17 2023 at 20:42):
This lets you reflect types even if their terms aren't in general reflectable.
This is the design we had in Lean 3 I think with docs#reflect vs docs#reflected (and also docs#reflected_value)
Kyle Miller (Apr 17 2023 at 21:03):
I got as far as getting exciting new errors with this version:
open Lean Qq in
unsafe instance toExpr (n : ℕ) : ToExpr (Fin n) where
toTypeExpr := q(Fin $n)
toExpr := match h : n with
| 0 => finZeroElim
| n' + 1 => fun i =>
let i' : Q(ℕ) := Lean.toExpr (i : ℕ)
let _n_eq : ($n' + 1) =Q $n := ⟨⟩
let pf : Q(NeZero $n) := q(@NeZero.succ $n')
q((haveI : NeZero $n := $pf; OfNat.ofNat $i' : Fin $n))
Error:
reduceEval: failed to evaluate argument
n
Alex Keizer (Apr 17 2023 at 21:08):
I was also thinking of making the value to be reflected a parameter to the class
import Mathlib.Tactic
import Mathlib.Tactic.ToExpr
import Qq
open Lean Qq
class ReflectType (α : Sort u) [ToLevel.{u}] where
reflectType : QQ (Expr.sort toLevel.{u})
class Reflect {α : Sort u} (a : α) [ToLevel.{u}] [ReflectType α] where
reflect : QQ (ReflectType.reflectType α)
export Reflect (reflect)
That would allow us to reflect specific terms of types that we may not know how to reflect in general. In particular, with some clever instances we can make reflect
work on types, too, making ReflectType
just an internal implementation detail.
instance [ToLevel.{u}] : ReflectType (Sort u) where
reflectType := .sort toLevel.{u}
instance {α : Sort u} [ToLevel.{u}] [r : ReflectType α] : Reflect α where
reflect := r.reflectType
instance {α : Sort u} [ToLevel.{u}] [Reflect α] : ReflectType α where
reflectType := reflect α
instance : Reflect Nat where
reflect := toTypeExpr Nat
instance {n : Nat} : Reflect n where
reflect := toExpr n
#check reflect Nat
#check reflect 1
Alex Keizer (Apr 17 2023 at 21:19):
With this version we can even (ab)use typeclass synthesis for some symbolic reasoning:
/--
Some structure that incorporates a function, thus cannot be reflected in general
-/
structure Foo where
n : Nat
f : Nat → Nat
/--
Construct a `Foo` from just a `Nat`, using `id` as the function
-/
def Foo.ofNat (n : Nat) : Foo := ⟨n, id⟩
instance : Reflect Foo where
reflect := q(Foo)
instance (n : Nat) : Reflect (Foo.ofNat n) where
reflect := q(Foo.ofNat $n)
section
variable (foo : Foo)
/-
Whoops, we cannot reflect `Foo` in general:
failed to synthesize instance
Reflect foo
-/
#check reflect foo
variable (n : Nat)
/-
But this does work!
-/
#check
let bar := Foo.ofNat n
reflect bar
end
I'm not sure actually defining these instances is a good idea, but it's cool!
Eric Wieser (Apr 18 2023 at 08:45):
Congrats, you've reinvented (a QQ-friendly version of) docs#reflected! But I agree that design looks useful, and I don't know why it was changed between Lean 3 and Lean 4
Eric Wieser (Jul 13 2023 at 11:58):
docs#Lean.ToLevel now exists. Is there a straightforward way to teach Qq about this? nevermind, I guess it already existed above!
Last updated: Dec 20 2023 at 11:08 UTC