Zulip Chat Archive

Stream: metaprogramming / tactics

Topic: passing functions


Damiano Testa (Apr 27 2022 at 17:24):

Dear All,

in the code below, to_oper reads an expression and converts has_mul.mul to has_add.add (the example shows how it works in a simple case).

import algebra.ring.basic

open tactic

/--  Convert the `has_mul.mul` in an expression to `has_add.add`. -/
meta def to_oper : expr  tactic expr
| `(has_mul.mul %%a %%b)     := do oa  to_oper a, ob  to_oper b,
                                  mk_app `has_add.add [oa, ob]
| (expr.lam a b c e)         := do oc  to_oper c, oe  to_oper e,
                                  return (expr.lam a b oc oe)
| (expr.pi  a b c e)         := do oc  to_oper c, oe  to_oper e,
                                  return (expr.pi  a b oc oe)
| (expr.elet a b c e)        := do ob  to_oper b, oc  to_oper c, oe  to_oper e,
                                  return (expr.elet  a ob oc oe)
| (expr.app a b)             := do oa  to_oper a, ob  to_oper b,
                                  return (expr.app oa ob)
| (expr.mvar u p e)          := do oe  to_oper e,
                                  return (expr.mvar u p oe)
| (expr.local_const u p b e) := do oe  to_oper e,
                                  return (expr.local_const u p b oe)
| (expr.macro md le)         := do ole  le.mmap to_oper,
                                  return (expr.macro md ole)
| e                          := return e


example {R : Type*} [comm_semiring R] {a b c d : R} : b * (a * (c * d)) = 1 :=
by do
  t  target,
  trace "before",
  trace t,
  trace "after",
  trace $ to_oper t

/-
before
b * (a * (c * d)) = 1
after
b + (a + (c + d)) = 1
-/

In this case has_mul.mul and has_add.add are hard-coded in to_oper, inside the first match pattern.

Would it be possible to provide instead two "generic" functions? Possibly, functions that do not even exist as declarations, simply making Lean pretend that what we are passing makes sense and checking it at execution time?

Thanks!

Arthur Paulino (Apr 27 2022 at 17:36):

From what I understand, you need to find a way to receive the function names as terms of name (I believe there's a proper parser for this). Then, instead of mk_app `has_add.add [oa, ob] you'd do mk_app my_fun_name [oa, ob].

But in order to be able to match with `(has_mul.mul %%a %%b) like that, I think you need to pass your function as an expression my_fun2_expr so you'd do `(%%my_fun2_expr %%a %%b).

And you should be able to build my_fun2_expr with let my_fun2_expr := mk_const my_fun2_name. This is all untested, but i'd try this approach

Damiano Testa (Apr 27 2022 at 17:40):

Ok, thanks! I was trying something like this, but was lacking the name trick. I will try harder!

Arthur Paulino (Apr 27 2022 at 17:43):

I took the matching idea from the sixth bullet point of Mario's backtick cheat sheet. Maybe @Mario Carneiro himself can validate the idea and shed more light on the matter

Damiano Testa (Apr 27 2022 at 19:10):

I haven't yet managed to make it work, but I do feel closer, thanks!

Alex J. Best (Apr 27 2022 at 19:57):

It should be possible to get rid of some of the boilerplate here using docs#expr.replace or docs#expr.mreplace I believe

Damiano Testa (Apr 28 2022 at 01:36):

Alex, thanks! I'll play with this, although I am a little conscious about the bound on binders: I don't know exactly what the consequences of that are.

E.g. is it 2 in my case, since I have binary operations, or is it unbounded, since I want to recurse into everything? I'll try to sort this out, once I have something that works!

Damiano Testa (Apr 28 2022 at 03:30):

This is where I got so far:

meta def to_oper : name  name  expr  tactic expr
| f₁ f₂ e := let f₁_expr := mk_const f₁ in
match e with
  | `(has_mul.mul %%a %%b)     := do oa  to_oper f₁ f₂ a, ob  to_oper f₁ f₂ b,
                                    mk_app f₂ [oa, ob]
[same as before]
  end

The rhs of the match works as expected!

The lhs is still the hard-coded has_mul.mul, so, right now f₁ just rides along: it gets converted to an expr but not used otherwise.

If I replace

  | `(has_mul.mul %%a %%b)     :=

with

  | `(%%f₁_expr %%a %%b)     :=

Lean complains:

function expected at
  _x_1
term has type
  ?m_1
-- underlines `f₁_expr`

My understanding is that Lean does not trust that I will pass a function of two variables as an f₁_expr, but I do not know how to reassure it that this is ok!

Arthur Paulino (Apr 28 2022 at 03:38):

Ouch, seems like my idea didn't work. You might still be able to use expr.is_app_of or expr.is_napp_of there

(not exactly there, of course... the code will need some restructuring, but nothing too serious)

Arthur Paulino (Apr 28 2022 at 03:51):

Something along the lines of

meta def to_oper : name  name  expr  tactic expr
| f₁ f₂ e := do
  if e.is_napp_of f₁ 2 then
    oa  to_oper f₁ f₂ (e.ith_arg 0),
    ob  to_oper f₁ f₂ (e.ith_arg 1),
    mk_app f₂ [oa, ob]
  else
    match e with
      [same as before except for the first match]
    end

[untested!]

Arthur Paulino (Apr 28 2022 at 03:54):

(deleted)

Mario Carneiro (Apr 28 2022 at 03:57):

instead of using syntax quotations, you can write expr.app (expr.app f₁_expr a) b := ...

Damiano Testa (Apr 28 2022 at 04:21):

Mario, I tried this, but it matched everything, even the =.

Damiano Testa (Apr 28 2022 at 04:34):

Anyway, I'm going to be back to my computer in a little bit, and I am going to try exactly what you suggest.

When I had tried passing the converted name and also directly assuming and expr and passing it, it was always extremely eager, ignoring the input that I was giving.

I probably had a mistake somewhere else, though.

Mario Carneiro (Apr 28 2022 at 04:38):

I've lost track of what you are trying to do. I was responding to this:
Damiano Testa said:

If I replace

  | `(has_mul.mul %%a %%b)     :=

with

  | `(%%f₁_expr %%a %%b)     :=

Lean complains:

If you use expr.app you can use it to write that pattern match

Damiano Testa (Apr 28 2022 at 04:39):

Ok, once I'm back at my computer I will try this and will report! Thanks!

Mario Carneiro (Apr 28 2022 at 04:40):

Of course such a match will match all sorts of binary operators, including =. Not sure how you want to search for multiplication-like operators

Damiano Testa (Apr 28 2022 at 04:41):

I would like to provide the binary operator myself, as a parameter and only match that operator. Would that be possible?

Mario Carneiro (Apr 28 2022 at 04:41):

you can match expr.app (expr.app f a) b and then say if f = my_op then ... else fallback

Damiano Testa (Apr 28 2022 at 04:41):

(What you are describing is exactly what had happened then: it matched also the equal sign, even if I had given it has_mul.mul as a name.

Mario Carneiro (Apr 28 2022 at 04:42):

or perhaps f.get_app_fn.const_name or so

Damiano Testa (Apr 28 2022 at 04:42):

Ok, you've given me some more ideas: I'll try to make them work! Thanks!

Damiano Testa (Apr 28 2022 at 04:43):

I understand maybe: even if the typed symbol inside the match coincides with the external name, lean treats is as different, right?

Mario Carneiro (Apr 28 2022 at 04:43):

since lean doesn't have pattern guards you will want to express this as a cascaded match

Damiano Testa (Apr 28 2022 at 04:43):

This is the reason for the ite, am I correct?

Mario Carneiro (Apr 28 2022 at 04:44):

The match will give you any three expressions in the form f a b, lots of subterms have that form

Damiano Testa (Apr 28 2022 at 04:44):

(this at least is compatible with the fact that I saw it matching everything)

Mario Carneiro (Apr 28 2022 at 04:44):

you then want to narrow down to the case where f is exactly the op you are looking for

Damiano Testa (Apr 28 2022 at 04:44):

Yes, the lots of matches is what I observed.

Mario Carneiro (Apr 28 2022 at 04:45):

you can't put the variable directly in the pattern, you just have to say f = my_op

Mario Carneiro (Apr 28 2022 at 04:45):

if my_op was a known literal like has_mul.mul then you could put it directly in the match

Damiano Testa (Apr 28 2022 at 04:46):

Ok, this has clarified quite a bit of syntax for me, thank you so much! I am now much more confident that this will work and moreover I understand what I thought was weird behaviour earlier!

Damiano Testa (Apr 28 2022 at 04:46):

Yes, inputting directly the has_mul.mul worked, but was a limitation that I did not want.

Damiano Testa (Apr 28 2022 at 04:48):

Until about 10 days ago, I did not really know what a match was, now, I want to do it everywhere and am surprised when it does not magically work like I want it to!

This has been very helpful: I can't wait to put it in practice! :smile:

Damiano Testa (Apr 28 2022 at 07:15):

This works:

import data.nat.basic

open tactic

/--  Convert every occurrence of the binary operation with name `f₁` in an expression to
the binary operation with name `f₂`. -/
meta def to_oper (f₁ f₂ : name) : expr  tactic expr
| (expr.app (expr.app op a) b) := do oa  to_oper a, ob  to_oper b,
                                    if op.get_app_fn.const_name = f₁ then
                                       (mk_app f₂ [oa, ob]) else (return (op oa ob))
--  The rest of the match is unchanged
| (expr.lam a b c e)         := do oc  to_oper c, oe  to_oper e,
                                  return (expr.lam a b oc oe)
| (expr.pi  a b c e)         := do oc  to_oper c, oe  to_oper e,
                                  return (expr.pi  a b oc oe)
| (expr.elet a b c e)        := do ob  to_oper b, oc  to_oper c, oe  to_oper e,
                                  return (expr.elet  a ob oc oe)
| (expr.app a b)             := do oa  to_oper a, ob  to_oper b,
                                  return (expr.app oa ob)
| (expr.mvar u p e)          := do oe  to_oper e,
                                  return (expr.mvar u p oe)
| (expr.local_const u p b e) := do oe  to_oper e,
                                  return (expr.local_const u p b oe)
| (expr.macro md le)         := do ole  le.mmap (to_oper),
                                  return (expr.macro md ole)
| e                          := return e

example {a b c : } : b * (a * c) = b + (a + c) :=
by do
  t  target,
  trace "before",
  trace t,
  trace "after",
  trace $ to_oper `has_add.add `monoid.npow t

/-
before
b * (a * c) = b + (a + c)
after
b * (a * c) = monoid.npow b (monoid.npow a c)
-/

Thank you all for your insights!
:octopus: :tada:


Last updated: Dec 20 2023 at 11:08 UTC