File: SimpleRecordUpdate.v

package info (click to toggle)
coq-record-update 0.3.6-2
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 208 kB
  • sloc: makefile: 38; sed: 6
file content (167 lines) | stat: -rw-r--r-- 6,307 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
(*|
============================
How coq-record-update works
============================

A detailed explanation of how coq-record-update is implemented. This is a re-implementation that omits the safety checks in the actual implementation, so that the basic story is as clear as possible.

This explanation interleaves a simplified re-implementation of the library with demo modules that demonstrate features as they are added. These demo modules each import the previous, but we hide that command to avoid cluttering the output.

First, our basic goal is to implement the following typeclass for each record type `R` and projection function `proj`. The implicit arguments are set up so that setting a field `A` is as simple as `set A (fun a => a + 1) x`, which will increment the field `A` in a record `x`.
|*)

Class Setter {R T:Type} (proj: R -> T) :=
  set : (T -> T) -> (R -> R).

Arguments set {R T} proj {_} _ _ : assert.
Global Hint Mode Setter - - + : typeclass_instances.

Module demo1.

(*|
My favorite running example, a simple record with three fields.
|*)

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

(*|
With just the above definition, we would still need to implement the typeclass for every field of every record. Here's the particular form of implementation that we'll automate with this library.
|*)

  #[export] Instance setA : Setter A :=
    fun (f:nat -> nat) (x:X) => mkX (f (A x)) (B x) (C x).
  #[export] Instance setB : Setter B :=
    fun (f:nat -> nat) (x:X) => mkX (A x) (f (B x)) (C x).
  #[export] Instance setC : Setter C :=
    fun (f:bool -> bool) (x:X) => mkX (A x) (B x) (f (C x)).
End demo1.

(*|
The basis for the automation will be to extract the common parts of the above, an "eta expansion" that doesn't set any fields but reconstructs a record from all of its fields. The user will provide the eta expansion by implementing the `Settable` typeclass.
|*)

Class Settable (R: Type) :=
  mkRecord : R -> R.

Arguments mkRecord R _ _ : assert, clear implicits.
Global Hint Mode Settable + : typeclass_instances.

Module demo2.
  Import demo1. (* .none *)

(*|
Here's how we intend to implement `Settable`. This is equal to `fun x => x`, but we won't actually call `mkRecord`, instead we'll look up the implementation of `Settable` and actually look at the definition (rather than using the instances opaquely).
|*)

  #[export] Instance etaX : Settable X :=
    fun x => mkX (A x) (B x) (C x).
End demo2.

(*|
Looking up a typeclass instance is pretty simple if you think about it: we just typecheck `_ : Settable R`, which will trigger typeclass resolution to fill in the underscore!
|*)

Ltac get_eta R :=
  let eta := eval hnf in (_ : Settable R) in
  eta.

(*|
Given an eta expansion (recall they look like `fun x => mkX (A x) (B x) (C
x)`), we want to substitute an update function in the place of some projection
function `proj`. The way we can do that is with the `pattern` tactic.

Let's say we call `make_setter eta B`, where `eta` is the above eta expansion.
`setter_r` factors it into `(fun B_f => mkX (A x) (B_f x) (C x)) B`. This is
almost what we want, except the `set` function doesn't allow any function of the
whole record, only of `B x`, so we actually use `fun r => f (B r)` as the
replacement for `B` (note that this is just `f ∘ B`, expanded out).
|*)

Ltac make_setter eta proj :=
  let setter_r := (eval pattern proj in eta) in
  lazymatch setter_r with
  | ?set_f _ =>
    let setter := constr:(fun f => set_f (fun r => f (proj r))) in
    (* we can clean up the actual setter term by beta-reducing it *)
    let setter := (eval cbn beta in setter) in
    setter
  end.

Module demo3.
  Import demo1 demo2. (* .none *)

(*|
Let's see what the above tactics do before we tie everything together into a nice user interface.

Recall that we've already implemented `Settable X`, so `get_eta X` can look it up.
|*)

  Goal True. (* .in .messages *)
    let eta := get_eta X in
    idtac eta. (* .in .messages .unfold *)
    let eta := get_eta X in
    let setter_B := make_setter eta B in
    idtac setter_B. (* .in .messages .unfold *)
  Abort.

End demo3.

(*|
Finally, we want to use `make_setter` without any fancy syntax. The way we do that is to implement the `Setter` typeclass using Ltac, rather than the usual mechanism of adding definitions as instances. Instance resolution turns out to just be a (slightly modified) `eauto` search using the `typeclass_instances` hint database, so we can register a `Hint Extern` to use `make_setter` to resolve `Setter`.

First we package up `get_eta` and `make_setter` into a single tactic that will solve goals of the form `Setter proj`, as long as `Settable` is implemented for the relevant record type.
|*)

Ltac solve_setter R proj :=
  let eta := get_eta R in
  let setter := make_setter eta proj in
  exact setter.

(*|
Now we add `solve_setter` as a way to prove `Setter` during typeclass resolution.
|*)

Global Hint Extern 1 (@Setter ?R _ ?proj) =>
         solve_setter R proj : typeclass_instances.

(*|
To make the library usable, we provide a notation that builds the eta expansion from a list of record fields, which is much easier to type than the actual eta expansion.
|*)

Notation "'settable!' mk < f1 ; .. ; fn >" :=
  (fun x => .. (mk (f1 x)) .. (fn x))
    (at level 0, mk at level 10, f1, fn at level 9, only parsing).

Module demo4.
  Import demo1. (* .none *)

(*|
Before we had to write out the expansion of X carefully; now we can just list out the constructor and fields:
|*)

  #[export] Instance etaX : Settable _ := settable! mkX <A;B;C>.
End demo4.

(*|
We also provide notations for the `set` function that make multiple updates, and updates to constants easier to read and write.
|*)

Notation "x <| proj ::= f |>" :=
  (set proj f x)
    (at level 12, f at next level, left associativity,
    format "x  <| proj  ::=  f |>").
Notation "x <| proj := v |>" :=
  (set proj (fun _ => v) x)
    (at level 12, left associativity,
    format "x  <| proj  :=  v |>").

Module test.
  Record X := mkX { A: nat;
                    B: nat;
                    C: unit }.

  #[export] Instance eta : Settable X := settable! mkX <A;B;C>.

  Definition setAB a b x := x <|A := a|> <|B := b|>.
  Definition updateAB a b x := x <|A ::= plus a|> <|B ::= minus b|>.
End test.