Zulip Chat Archive

Stream: Is there code for X?

Topic: Simplify lambda expressions


Jeremy Salwen (May 10 2023 at 03:23):

Here is a MWE. I would like to simplify the LHS of the equation to just f. (RHS is irrelevant).

import Lean
import Mathlib.Tactic.Find

def f:      := sorry
def asdf : (λ a => f a) = (λ a => f (f a)) := by simp

unfortunately simp does nothing here. It seems like there were a few previous threads about reducing these sorts of expressions (e.g. https://leanprover.zulipchat.com/#narrow/stream/113489-new-members/topic/Simplifying.20lambda.20expressions) and that the conclusion was that this simplification is called beta reduction and dsimp only or simp only should handle it, but that does not seem to be happening here.

Chris Bailey (May 10 2023 at 03:59):

You need to give it a little bit more direction, I think.

def asdf : (λ a => f a) = (λ a => f (f a)) := by
  simp only [show (λ a => f a) = f by rfl]
  trace_state

Jeremy Salwen (May 10 2023 at 04:38):

Hmm, this doesn't seem ideal. Why is simp unable to do the beta reduction? In general do I need to manually clean up the beta reductions that simp fails to do? Or is there another tactic for this scenario?

Chris Bailey (May 10 2023 at 04:52):

This is not beta reduction, it's eta reduction/expansion. The definitional equality procedure checks eta equivalence which is why rfl works, but simp needs to know what you want to rewrite with in this case.

Chris Bailey (May 10 2023 at 04:54):

There's an eta := true option for the simp config, but I'm not sure how it works.

Yaël Dillies (May 10 2023 at 06:22):

simp (config := { eta := true }) should do, if not unacceptably long

Jeremy Salwen (May 10 2023 at 15:28):

Yaël Dillies said:

simp (config := { eta := true }) should do, if not unacceptably long

I tried this, it still doesn't have any effect.

Jeremy Salwen (May 11 2023 at 16:07):

I've tried searching more through zulip and the mathlib docs, and came up short. #reduce doesn't simplify this expression either.

There is the Expr.etaExpanded? function, which I can use to calculate the eta-reduced form of a a type:

def transformEtaReduce (e: Expr): MetaM Expr := do
  transform e (λ node => do
  pure (match node.etaExpanded? with
        | some reduced =>
          TransformStep.continue reduced
        | none => TransformStep.continue
        ))

but this is not actually generating proof terms.

Kyle Miller (May 11 2023 at 17:16):

Oh, now that you've written that, it's not so hard to wrap an interactive tactic around it:

import Lean

open Lean Meta Elab.Tactic

def transformEtaReduce (e: Expr): MetaM Expr := do
  transform e fun node => pure <| TransformStep.continue node.etaExpanded?

syntax "eta_reduce" : tactic

elab_rules : tactic
  | `(tactic| eta_reduce) =>
    liftMetaTactic1 fun mvarId => do
      let ty  instantiateMVars ( mvarId.getType)
      -- Eta reduction is defeq:
      mvarId.change ( transformEtaReduce ty)

def f: Nat   Nat  := sorry
def asdf : (λ a => f a) = (λ a => f (f a)) := by
  eta_reduce
  -- goal: f = fun a ↦ f (f a)

Kyle Miller (May 11 2023 at 17:19):

With a little more work you can support eta_reduce at h syntax too.

Kyle Miller (May 11 2023 at 17:24):

Here, with locations:

import Mathlib.Tactic.Basic

open Lean Meta Elab.Tactic

def transformEtaReduce (e: Expr): MetaM Expr := do
  transform e fun node => pure <| TransformStep.continue node.etaExpanded?

syntax "eta_reduce" (ppSpace Parser.Tactic.location)? : tactic

elab_rules : tactic
  | `(tactic| eta_reduce $[$loc?]?) =>
  withLocation (expandOptLocation (Lean.mkOptionalNode loc?))
    (atLocal := fun h => liftMetaTactic1 fun mvarId => do
      let ty  instantiateMVars ( h.getType)
      mvarId.changeLocalDecl' h ( transformEtaReduce ty))
    (atTarget := liftMetaTactic1 fun mvarId => do
      let ty  instantiateMVars ( mvarId.getType)
      mvarId.change ( transformEtaReduce ty))
    (failed := fun _ => throwError "eta_reduce failed")

def f: Nat   Nat  := sorry
theorem asdf : (λ a => f a) = (λ a => f (f a)) := by
  eta_reduce
  -- goal: f = fun a ↦ f (f a)

theorem asdf' (h : (λ a => f a) = (λ a => f (f a))) : True := by
  eta_reduce at h
  -- h : f = fun a ↦ f (f a)

theorem asdf'' (h : (λ a => f a) = (λ a => f (f a))) : (λ a => f a) = (λ a => f (f a)) := by
  eta_reduce at *
  -- h : f = fun a ↦ f (f a)
  -- ⊢ f = fun a ↦ f (f a)

Kyle Miller (May 11 2023 at 17:39):

An eta_expand would be interesting too, but to do that right you'd need a transformer that processes entire function applications. This transformer expands every function:

def transformEtaExpand (e : Expr) : MetaM Expr := do
  transform e (post := fun node => TransformStep.done <$> Meta.etaExpand node)

That gives some silly results:

example : (fun (a b : Nat) => a + b) = (· + ·) := by
  eta_expand
  -- goal: (fun {α} a a_1 ↦ a = a_1) (fun a b ↦ (fun a b ↦ (fun {α β} {γ}
  --   [HAdd α β γ] a a_1 ↦ a + a_1) a b) a b) fun x x_1 ↦ (fun x x_2 ↦ (fun {α β} {γ} [HAdd α β γ]
  --   a a_1 ↦ a + a_1) x x_2) x x_1

Beta reducing the result fixes that, but maybe there's a better way?

def transformEtaExpand (e : Expr) : MetaM Expr := do
  let e'  transform e (post := fun node => TransformStep.done <$> Meta.etaExpand node)
  Core.betaReduce e'

This at least works as an inverse to eta_reduce:

example : (fun (a b : Nat) => a + b) = (· + ·) := by
  -- goal: (fun a b ↦ a + b) = fun x x_1 ↦ x + x_1
  eta_reduce
  -- goal: HAdd.hAdd = HAdd.hAdd   (has implicit arguments for Nat instances)
  eta_expand
  -- goal: (fun a a_1 ↦ a + a_1) = fun a a_1 ↦ a + a_1

Kyle Miller (May 11 2023 at 17:39):

Here are both tactics together:

code

Kevin Buzzard (May 11 2023 at 17:40):

well and truly nerdsniped :-) Very nice!

Jeremy Salwen (May 11 2023 at 17:50):

For the transformEtaExpand would it make sense to do the beta reduction as part of the transform so it's single pass? Like this?

def betaReduceHead (e:Expr): Expr :=
  if e.isHeadBetaTarget then e.headBeta else e

def transformEtaExpand (e : Expr) : MetaM Expr := do
  let e'  transform e (post := fun node => TransformStep.done <$> betaReduceHead <$> Meta.etaExpand node)
  Core.betaReduce e'

Kyle Miller (May 11 2023 at 17:59):

The thing this is working around is that if you have an expression f x, then we first look at f and we get (fun y => f y) x, so then when we look at this whole application we need to beta reduce it. Beta reducing as part of the post pass will work, but really it'd be better to never do these sorts of eta expansions in the first place so that we never need to beta reduce.


Last updated: Dec 20 2023 at 11:08 UTC