Derive handler for fintype
instances #
This file introduces a derive handler to automatically generate fintype
instances for structures and inductives.
Implementation notes #
To construct a fintype instance, we need 3 things:
- A list
l
of elements - A proof that
l
has no duplicates - A proof that every element in the type is in
l
Now fintype is defined as a finset which enumerates all elements, so steps (1) and (2) are bundled together. It is possible to use finset operations that remove duplicates to avoid the need to prove (2), but this adds unnecessary functions to the constructed term, which makes it more expensive to compute the list, and it also adds a dependence on decidable equality for the type, which we want to avoid.
Because we will rely on fintype instances for constructor arguments, we can't actually build a list directly, so (1) and (2) are necessarily somewhat intertwined. The inductive types we will be proving instances for look something like this:
The list of elements that we generate is
{foo.zero}
∪ (finset.univ : bool).map (λ b, finset.one b)
∪ (finset.univ : Σ' x : fin 3, bar x).map (λ ⟨x, y⟩, finset.two x y)
except that instead of ∪
, that is finset.union
, we use finset.disj_union
which doesn't
require any deduplication, but does require a proof that the two parts of the union are disjoint.
We use finset.cons
to append singletons like foo.zero
.
The proofs of disjointness would be somewhat expensive since there are quadratically many of them, so instead we use a "discriminant" function. Essentially, we define
def foo.enum : foo → ℕ
| foo.zero := 0
| (foo.one _) := 1
| (foo.two _ _) := 2
and now the existence of this function implies that foo.zero is not foo.two and so on because they
map to different natural numbers. We can prove that sets of natural numbers are mutually disjoint
more easily because they have a linear order: 0 < 1 < 2
so 0 ≠ 2
.
To package this argument up, we define finset_above foo foo.enum n
to be a finset s
together
with a proof that all elements a ∈ s
have n ≤ enum a
. Now we only have to prove that
enum foo.zero = 0
, enum (foo.one _) = 1
, etc. (linearly many proofs, all rfl
) in order to
prove that all variants are mutually distinct.
We mirror the finset.cons
and finset.disj_union
functions into finset_above.cons
and
finset_above.union
, and this forms the main part of the finset construction.
This only handles distinguishing variants of a finset. Now we must enumerate the elements of a
variant, for example {foo.one ff, foo.one tt}
, while at the same time proving that all these
elements have discriminant 1
in this case. To do that, we use the finset_in
type, which
is a finset satisfying a property P
, here λ a, foo.enum a = 1
.
We could use finset.bind
many times to construct the finset but it turns out to be somewhat
complicated to get good side goals for a naturally nodup version of finset.bind
in the same way
as we did with finset.cons
and finset.union
. Instead, we tuple up all arguments into one type,
leveraging the fintype
instance on psigma
, and then define a map from this type to the
inductive type that untuples them and applies the constructor. The injectivity property of the
constructor ensures that this function is injective, so we can use finset.map
to apply it. This
is the content of the constructor finset_in.mk
.
That completes the proofs of (1) and (2). To prove (3), we perform one case analysis over the inductive type, proving theorems like
foo.one a ∈ {foo.zero}
∪ (finset.univ : bool).map (λ b, finset.one b)
∪ (finset.univ : Σ' x : fin 3, bar x).map (λ ⟨x, y⟩, finset.two x y)
by seeking to the relevant disjunct and then supplying the constructor arguments. This part of the
proof is quadratic, but quite simple. (We could do it in O(n log n)
if we used a balanced tree
for the unions.)
The tactics perform the following parts of this proof scheme:
mk_sigma
constructs the typeΓ
infinset_in.mk
mk_sigma_elim
constructs the functionf
infinset_in.mk
mk_sigma_elim_inj
proves thatf
is injectivemk_sigma_elim_eq
proves that∀ a, enum (f a) = k
mk_finset
constructs the finsetS = {foo.zero} ∪ ...
by recursion on the variantsmk_finset_total
constructs the proof|- foo.zero ∈ S; |- foo.one a ∈ S; |- foo.two a b ∈ S
by recursion on the subgoals coming out of the initialcases
mk_fintype_instance
puts it all together to produce a proof offintype foo
. The construction offoo.enum
is also done in this function.
A step in the construction of finset.univ
for a finite inductive type.
We will set enum
to the discriminant of the inductive type, so a finset_above
represents a finset that enumerates all elements in a tail of the constructor list.
Instances for derive_fintype.finset_above
This is the case for a simple variant (no arguments) in an inductive type.
Equations
- derive_fintype.finset_above.cons n a h s = ⟨finset.cons a s.val _, _⟩
The base case is when we run out of variants; we just put an empty finset at the end.
Equations
Equations
This is a finset covering a nontrivial variant (with one or more constructor arguments).
The property P
here is λ a, enum a = n
where n
is the discriminant for the current
variant.
Equations
- derive_fintype.finset_in P = {s // ∀ (x : α), x ∈ s → P x}
To construct the finset, we use an injective map from the type Γ
, which will be the
sigma over all constructor arguments. We use sigma instances and existing fintype instances
to prove that Γ
is a fintype, and construct the function f
that maps ⟨a, b, c, ...⟩
to C_n a b c ...
where C_n
is the nth constructor, and mem
asserts
enum (C_n a b c ...) = n
.
Equations
- derive_fintype.finset_in.mk Γ f inj mem = ⟨finset.map {to_fun := f, inj' := inj} finset.univ, _⟩
For nontrivial variants, we split the constructor list into a finset_in
component for the
current constructor and a finset_above
for the rest.
Equations
- derive_fintype.finset_above.union n s t = ⟨s.val.disj_union t.val _, _⟩