-
Notifications
You must be signed in to change notification settings - Fork 813
RFC: Add left actions and right actions to expression tree elaborator and make ^ be a right action #2854
Description
Summary
- Extend the
Lean.Elab.Extraexpression tree elaborator (which processesbinop%,unop%, etc.) withleftact%andrightact%, which are for left actions (operations of the formα → β → β) and right actions (operations of the formα → β → α). - Switch
^notation from usingbinop%to usingrightact%. - Add
NatPowandHomogeneousPowclasses that provide defaultPowinstances for theα → Nat → αandα → α → αcases. AddNatPow NatandHomogeneousPow Floatinstances.
Motivation
The ^ operator currently uses binop%, which causes the notation to preferentially use a Pow α α instance if it exists. This leads to issues throughout mathlib -- and to repeated questions on Zulip -- because more often one wants x ^ 2 to elaborate using a Pow α Nat instance if it exists rather than using a Pow α α instance. For illustration, if x : Real, then x ^ 2 elaborates with 2 : Real rather than 2 : Nat because (1) there is a Pow Real Real instance and (2) the expression tree elaborator tries to make binop% 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:
example (x : Real) : x ^ 2 = x * x := by
-- rw [pow_two] by itself fails since it needs `2 : Nat` to apply
-- Instead, need to go through some coercions:
rw [← Nat.cast_ofNat, Real.rpow_nat_cast, pow_two]Actions
Mathematically speaking, ^ is an example of a right action of Nat on a multiplicative monoid M. That is to say, whenever a type has an associative multiplication operation and a multiplicative unit, it makes sense to define ^ as a function M → Nat → M by the rules a ^ 0 = 1 and a ^ (n + 1) = a ^ n * a, and this action supports nice lemmas such as a ^ (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 field k and specializing it to Real 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 of HPow, 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 as Pow Real Nat as that would cause 3 ^ 2 to elaborate with 3 : Real rather than 3 : Nat. Third, this logic only applies if there's a fully homogenous instance. Rather than finding some hack that would make binop% work in this case, it is easier to implement true support for left and right actions.
Making ^ elaborate with rightact% rather than binop% 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 to rightact% 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 the SMul notation for module actions to participate in the expression tree elaborator. Here is an example:
variable [Ring R] [AddCommMonoid M] [Module R M] (r : R) (N : Submodule R M) (m : M) (n : N)
#check m + r • nWith the current SMul notation, which does not participate in the expression tree elaborator at all, this elaborates as m + ↑(r • n : ↑N) : M. Making SMul use leftact% allows this to elaborate as m + r • (↑n : M) : M, like other arithmetic expressions. Just as is the case for Pow, sometimes SMul too has fully homogeneous instances, like from the canonical Module R R instance of a ring acting on itself by multiplication, but this is not the preferred instance when working with actions.
Nat and Float elaboration issues
While 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 uses Float wants everything in sight to be a Float, 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 to Nat 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 that a is a Nat due to the fact that 2 defaults to being a Nat then this would fail to elaborate, so ideally there would be some way to hint in this case that the exponent is a Nat. 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 a Nat (this is consistent with ∃ a b, a * a = b not elaborating), and in fact this elaborating with a Nat 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 and HomogeneousPow classes that implement default instances for Pow, and then by having a NatPow Nat instance and a HomogeneousPow 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 adding
local 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 the binop% operator to elaborate with the wanted type for the exponent. Adding a rightact% elaborator makes it so these workarounds can be removed, resolving #2220.
Manual-level explanation
The elaborator provides binop%, binop_lazy%, leftact%, rightact%, and unop% 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 belaboratesf a bas a binary operator with two operandsaandb, and each operand participates in the protocol.binop_lazy% f a bis likebinop%but elaborates asf a (fun () => b).unop% f aelaboratesf aas a unary operator with one operanda, which participates in the protocol.leftact% f a belaboratesf a bas a left action (theaoperand "acts upon" theboperand). Onlybparticipates in the protocol sinceacan have an unrelated type, for example scalar multiplication of vectors.rightact% f a belaboratesf a bas a right action (theboperand "acts upon" theaoperand). Onlyaparticipates in the protocol sincebcan have an unrelated type. This is used byHPowsince, for example, there are bothReal -> Nat -> RealandReal -> Real -> Realexponentiation functions, and we prefer the former in the case ofx ^ 2.- There are also
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:
macro_rules | `($x + $y) => `(binop% HAdd.hAdd $x $y)
macro_rules | `($x ^ $y) => `(rightact% HPow.hPow $x $y)
macro_rules | `(- $x) => `(unop% Neg.neg $x)The elaborator assumes that each function is as heterogenous as possible, for example HAdd.hAdd : α → β → γ where HAdd 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:
class Pow (α : Type u) (β : Type v) where
pow : α → β → α
class NatPow (α : Type u) where
pow : α → Nat → α
class HomogeneousPow (α : Type u) where
pow : α → α → αPow provides the default instance associated to how Pow is generally a right action. Both NatPow and HomogeneousPow provide a default Pow 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 a Nat or to the base's type. There is a NatPow Nat instance and a HomogeneousPow Float instance in the core library.
Reference-level explanation
The expression tree elaborator handles leftact% and rightact% in the following way:
- For
leftact% f a b, the termais a leaf andbis processed as a tree. During type analysis, the algorithm only looks atb's tree. During coercion insertion, coercions are only inserted inb's tree. - For
rightact% f a b, the termais processed as a tree andbis 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:- std does not need any fixes
- we commit to fixing issues in mathlib that arise from this change
- we have
NatPowandHomogeneousPowto mitigate regressions that might otherwise result in "normal" Lean use
-
Adding the
NatPowandHomogeneousPowclasses 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
Gis a group andg : G, it makes sense to writeg ^ (-2)for(inv g)^2. However, theNatPowdefault instance causes-2to be aNatand 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:
variable (b : Bool) (x y : Nat)
#check (1 : Int) + bif b then x else y
-- 1 + Int.ofNat (bif b then x else y) : IntOne could imagine that the result should instead be 1 + bif b then Int.ofNat x else Int.ofNat y.
Community Feedback
The leftact% and rightact% 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.