RFC: Add left actions and right actions to expression tree elaborator and make ^
be a right action
#2854
Labels
RFC
Request for comments
^
be a right action
#2854
Summary
Lean.Elab.Extra
expression tree elaborator (which processesbinop%
,unop%
, etc.) withleftact%
andrightact%
, which are for left actions (operations of the formα → β → β
) and right actions (operations of the formα → β → α
).^
notation from usingbinop%
to usingrightact%
.NatPow
andHomogeneousPow
classes that provide defaultPow
instances for theα → Nat → α
andα → α → α
cases. AddNatPow Nat
andHomogeneousPow Float
instances.Motivation
The
^
operator currently usesbinop%
, which causes the notation to preferentially use aPow α α
instance if it exists. This leads to issues throughout mathlib -- and to repeated questions on Zulip -- because more often one wantsx ^ 2
to elaborate using aPow α Nat
instance if it exists rather than using aPow α α
instance. For illustration, ifx : Real
, thenx ^ 2
elaborates with2 : Real
rather than2 : Nat
because (1) there is aPow Real Real
instance and (2) the expression tree elaborator tries to makebinop%
operations be fully homogeneous.This is behavior is unwanted because generally there are more theorems about
Nat
exponents. To use these theorems, one needs to apply coercion lemmas first, and this is not obvious to newer users who might not realize that they need to check exactly how^
elaborated to make progress. Here's an example using mathlib:Actions
Mathematically speaking,
^
is an example of a right action ofNat
on a multiplicative monoidM
. That is to say, whenever a type has an associative multiplication operation and a multiplicative unit, it makes sense to define^
as a functionM → Nat → M
by the rulesa ^ 0 = 1
anda ^ (n + 1) = a ^ n * a
, and this action supports nice lemmas such asa ^ (n + m) = a ^ n * a ^ m
. The real numbers, though a complicated construction, are incidentally able to support a fully homogeneous^
operation. However, taking a theorem about an abstract fieldk
and specializing it toReal
can lead to it no longer typechecking since^
for this specific type elaborates differently. While we cannot expect every piece of syntax to support specialization like this, ideally arithmetic operations support it wherever possible as this is a way to help limit user confusion.While the expression tree elaborator has logic to handle heterogenous instances, it is for heterogenous default instances so another solution is necessary. There are a few reasons for this. First,
Pow
is already a heterogeneous default instance ofHPow
, so the logic is not particularly appropriate for this operation. Second, the logic is for default instances for types with constant heads, and we wouldn't want to introduce default instances such asPow Real Nat
as that would cause3 ^ 2
to elaborate with3 : Real
rather than3 : Nat
. Third, this logic only applies if there's a fully homogenous instance. Rather than finding some hack that would makebinop%
work in this case, it is easier to implement true support for left and right actions.Making
^
elaborate withrightact%
rather thanbinop%
means that the exponent is free to be of any type, and then default instances can specialize the exponent it if necessary. (For example, mathlib's Pow instance for monoids.)Having
leftact%
in addition torightact%
is not just a matter of symmetry. Mathlib's theory of modules over rings is a theory of left actions, and providing it would allow for theSMul
notation for module actions to participate in the expression tree elaborator. Here is an example:With the current
SMul
notation, which does not participate in the expression tree elaborator at all, this elaborates asm + ↑(r • n : ↑N) : M
. MakingSMul
useleftact%
allows this to elaborate asm + r • (↑n : M) : M
, like other arithmetic expressions. Just as is the case forPow
, sometimesSMul
too has fully homogeneous instances, like from the canonicalModule R R
instance of a ring acting on itself by multiplication, but this is not the preferred instance when working with actions.Nat
andFloat
elaboration issuesWhile we want the exponent to not influence types when elaborating an expression tree, there are some considerations to make sure reasonable expressions can elaborate at all.
First, consider
Float
. In programming applications, an expression tree that usesFloat
wants everything in sight to be aFloat
, even exponents, which helps ensure floating-point operations are used rather that, say, repeated multiplication if the exponent is accidentally a natural number. Expressions such as(2.2 ^ 2.2 : Float)
ideally would elaborate as well. If we made exponents default toNat
universally, then this would yield a frustrating "failed to synthesize instance OfScientific Nat" error.Second, consider
Nat
. In an expression such as∃ a b, 2 ^ a = b
, if we do not have a default instance that can hint thata
is aNat
due to the fact that2
defaults to being aNat
then this would fail to elaborate, so ideally there would be some way to hint in this case that the exponent is aNat
. However, we would not expect∃ a b, a ^ 2 = b
to elaborate because the base of the exponent is unconstrained by the mere fact that the exponent is aNat
(this is consistent with∃ a b, a * a = b
not elaborating), and in fact this elaborating with aNat
base would be problematic as it would conflict with, for instance, mathlib's Pow instance for monoids.These two ideals seem to be at odds. However, default instances can support a solution for both situations, without introducing regressions. In particular, we can define
NatPow
andHomogeneousPow
classes that implement default instances forPow
, and then by having aNatPow Nat
instance and aHomogeneousPow Float
instance, both motivating examples elaborate.Workarounds
Currently, mathlib employs a workaround throughout the library. Many files using
^
turn off the expression tree elaborator completely by addinglocal macro_rules | `($x ^ $y) => `(HPow.hPow $x $y)
This pairs with the Pow instance for monoids to get the exponent to default to a
Nat
. The reason for doing this is that it saves inserting convoluted workarounds into expressions to get thebinop%
operator to elaborate with the wanted type for the exponent. Adding arightact%
elaborator makes it so these workarounds can be removed, resolving #2220.Manual-level explanation
The elaborator provides
binop%
,binop_lazy%
,leftact%
,rightact%
, andunop%
syntax to mark syntax that should participate in the expression tree elaborator.At a high level, the elaborator tries to solve for a type that each of the operands in the expression tree can be coerced to, while taking into account the expected type for the entire expression tree. Once this type is computed (and if it exists), it inserts coercions where needed.
Here are brief descriptions of each of the operator types:
binop% f a b
elaboratesf a b
as a binary operator with two operandsa
andb
, and each operand participates in the protocol.binop_lazy% f a b
is likebinop%
but elaborates asf a (fun () => b)
.unop% f a
elaboratesf a
as a unary operator with one operanda
, which participates in the protocol.leftact% f a b
elaboratesf a b
as a left action (thea
operand "acts upon" theb
operand). Onlyb
participates in the protocol sincea
can have an unrelated type, for example scalar multiplication of vectors.rightact% f a b
elaboratesf a b
as a right action (theb
operand "acts upon" thea
operand). Onlya
participates in the protocol sinceb
can have an unrelated type. This is used byHPow
since, for example, there are bothReal -> Nat -> Real
andReal -> Real -> Real
exponentiation functions, and we prefer the former in the case ofx ^ 2
.binrel%
andbinrel_no_prop%
for relations; in particular, these are binary operations where the expected type plays no role in the expression tree elaboration.The way to cause notation to use the expression tree elaborator is to write a macro for it. Here are some examples from the core library:
The elaborator assumes that each function is as heterogenous as possible, for example
HAdd.hAdd : α → β → γ
whereHAdd
is a three-argument typeclass. Generally, these heterogenous typeclasses have default instances that specialize them to a more homogeneous version.In the case of
HPow
, there are three specializations:Pow
provides the default instance associated to how Pow is generally a right action. BothNatPow
andHomogeneousPow
provide a defaultPow
instance, and types can choose to implement an instance for one of these two depending on whether they would prefer the exponent to default to being aNat
or to the base's type. There is aNatPow Nat
instance and aHomogeneousPow Float
instance in the core library.Reference-level explanation
The expression tree elaborator handles
leftact%
andrightact%
in the following way:leftact% f a b
, the terma
is a leaf andb
is processed as a tree. During type analysis, the algorithm only looks atb
's tree. During coercion insertion, coercions are only inserted inb
's tree.rightact% f a b
, the terma
is processed as a tree andb
is a leaf. During type analysis, the algorithm only looks ata
's tree. During coercion insertion, coercions are only inserted ina
's tree.Drawbacks
This is a breaking change for how
^
is elaborated, and it impacts libraries such as mathlib due to workarounds they have employed to avoid the way it elaborates. However:NatPow
andHomogeneousPow
to mitigate regressions that might otherwise result in "normal" Lean useAdding the
NatPow
andHomogeneousPow
classes adds more complexity to the arithmetic typeclasses.There is a recent issue (explicit
↑
in coercions + infix operators = unfolding timeout #2831) involving timeouts due to interactions between coercion arrows and the expression tree elaborator. Making^
useleftact%
and fixing mathlib causes this issue to become more prominent, but there are workarounds such as using(... :)
notation to limit the scope of the expression tree elaborator until this issue is fixed.One mathlib use case this fails to support is if the exponent is an integer literal. For example, if
G
is a group andg : G
, it makes sense to writeg ^ (-2)
for(inv g)^2
. However, theNatPow
default instance causes-2
to be aNat
and then result in a "failed to synthesize instance Neg Nat" error. There is a workaround, which is to writeg ^ (-2 : Int)
.Future possibilities
We might want to design a system to allow more general interaction between other syntaxes and the expression tree elaborator. For example, users may be surprised when they find that
if
/bif
statements do not participate:One could imagine that the result should instead be
1 + bif b then Int.ofNat x else Int.ofNat y
.Community Feedback
The
leftact%
andrightact%
additions have been mentioned on Zulip without much discussion. The mathlib community is excited to have a solution to the^
elaboration issue.Impact
Add 👍 to issues you consider important. If others benefit from the changes in this proposal being added, please ask them to add 👍 to it.
The text was updated successfully, but these errors were encountered: