tchajed / coq-record-update

Library to create Coq record update functions
MIT License
42 stars 16 forks source link

Obtaining eta expansion of constructor without typeclasses #19

Open samuelgruetter opened 3 years ago

samuelgruetter commented 3 years ago

A while back, @JasonGross pointed out that you can obtain an eta expansion of a record's constructor without relying on user-provided typeclass instances like

(* all you need to do is provide something like this, listing out the fields of your record: *)
Instance etaX : Settable _ := settable! mkX <A; B; C>.

and his solution looks as follows:

Record X := mkX { A : nat ; B : nat ; C : bool ; }.

Ltac make_eta X :=
  let s
      := constr:(ltac:(
                   let x := fresh "x" in
                   intro x; unshelve eexists;
                   [ econstructor; destruct x | destruct x; reflexivity ])
                 : forall x : X, { x' : X | x' = x }) in
  lazymatch s with
  | fun x => exist _ (@?s x) _ => s
  end.

Check ltac:(let s := make_eta X in exact s).

It creates an eta-expanded version of the constructor mkX:

fun x : X =>
{|
  A := match x with
       | {| A := A; B := B; C := C |} => (fun (A0 _ : nat) (_ : bool) => A0) A B C
       end;
  B := match x with
       | {| A := A; B := B; C := C |} => (fun (_ B0 : nat) (_ : bool) => B0) A B C
       end;
  C := match x with
       | {| A := A; B := B; C := C |} => (fun (_ _ : nat) (C0 : bool) => C0) A B C
       end
|}
     : X -> X

However, @tchajed (and myself, too) did not like that solution because it does not reuse the existing getters, but creates new ones.

Now, for those who like Ltac2 hacks, here's a way to obtain an eta-expanded constructor that does reuse the existing getters:

Require Import Ltac2.Ltac2.
Require Ltac2.Option.

Module Import ModA.
  Record foo(A: Type)(n: nat) := {
    fieldA: unit;
    fieldB: n = n;
    fieldC: bool;
  }.
End ModA.

(* to show that the code below is robust against this kind of shadowing *)
Definition fieldA := @id.

Ltac2 rec strip_foralls(t: constr) :=
  match Constr.Unsafe.kind t with
  | Constr.Unsafe.Prod b u => let (bs, body) := strip_foralls u in (b :: bs, body)
  | _ => ([], t)
  end.

Ltac2 app_arg_count(t: constr) :=
  match Constr.Unsafe.kind t with
  | Constr.Unsafe.App f args => Array.length args
  | _ => 0
  end.

Ltac2 binder_to_field(qualification: ident list)(b: binder) :=
   Option.get (Env.get (List.append qualification [Option.get (Constr.Binder.name b)])).

Ltac2 field_names(ctor_ref: Std.reference) :=
  let ctor_type := Constr.type (Env.instantiate ctor_ref) in
  let (binders, result) := strip_foralls ctor_type in
  let n_type_args := app_arg_count result in
  let field_name_binders := List.skipn n_type_args binders in
  List.map (binder_to_field (List.removelast (Env.path ctor_ref))) field_name_binders.

Ltac2 constructor_of_record(t: constr) :=
  match Constr.Unsafe.kind t with
  | Constr.Unsafe.Ind ind inst =>
    Std.ConstructRef (Constr.Unsafe.constructor ind 0)
  | _ => Control.throw (Invalid_argument (Some (Message.of_constr t)))
  end.

Ltac2 Eval constructor_of_record constr:(foo).

Ltac2 mkApp(f: constr)(args: constr array) :=
  Constr.Unsafe.make (Constr.Unsafe.App f args).

Ltac2 eta(t: constr) :=
  let (h, args) := match Constr.Unsafe.kind t with
                   | Constr.Unsafe.App h args => (h, args)
                   | _ => (t, Array.empty ())
                   end in
  let ctor := constructor_of_record h in
  let getters := List.map (fun getterRef => mkApp (Env.instantiate getterRef) args)
                          (field_names ctor) in
  constr:(fun x: $t => ltac2:(
    let projections := List.map (fun getter => constr:($getter &x)) getters in
    let res := mkApp (mkApp (Env.instantiate ctor) args) (Array.of_list projections) in
    exact $res)).

Definition foo_eta(T: Type)(n: nat): foo T n -> foo T n :=
  ltac2:(let res := eta constr:(foo T n) in exact $res).

Print foo_eta.
(*
foo_eta =
fun (T : Type) (n : nat) (x : foo T n) =>
{| ModA.fieldA := ModA.fieldA T n x; fieldB := fieldB T n x; fieldC := fieldC T n x |}
     : forall (T : Type) (n : nat), foo T n -> foo T n
*)

Definition X_eta: X -> X :=
  ltac2:(let res := eta constr:(X) in exact $res).

Print X_eta.
(* creates an eta-expanded version of the constructor mkX, reusing the existing getters A, B, C:
X_eta = fun x : X => {| A := A x; B := B x; C := C x |}
     : X -> X
*)

I haven't tried yet whether it could replace the current typeclass-based approach, nor spent any thought on whether such code is morally acceptable, but just wanted to paste it here for whoever might find it entertaining :wink:

samuelgruetter commented 2 years ago

For the records (apologies for the bad pun), I've started using the above Ltac2 hack in this file, so users of it don't need to declare any eta instances any more, and I also added support for OCaml-like syntax for updating several fields at once, eg { oldRecord with fieldC := true; fieldA ::= Nat.add 2 }, as well as a simplifier that simplifies get-of-set, set-of-same-set, set-of-different-set, set-of-constructor, get-of-constructor etc. It's not yet tested & clean enough for a PR, but in case someone is about to reinvent that wheel, let it be known that some code already exists.