copied from Slack thread, let me know if you have questions!
This was mostly based on Reynold’s original paper on parametricity, if I recall correctly …
In most proof assistants, you can’t prove that all functions are parametric, even if you can’t intuitionistically write a function that violates it (in particular, in the absence of LEM).
So you can’t prove that the type ∀ A → (A → (A → A) → A)
is equivalent to the natural numbers, you actually have to define a sigma type of functions that are parametric (and you also have to quotient this by proofs of parametricity, I think), and then you can obtain your result.
The big downside is that this proof of parametricity is not universe-level polymorphic, so it works only up to some level! And you don’t know that the proof of parametricity is parametric itself. It’s very annoying.
In principle, though, it works well enough to take assumptions of parametricity, and the proof of parametricity for any particular function should be straight-forward.
But first we have to actually say what it means to be parametric …
The idea is that R×
, R→
, and R∀
define the shapes of parametricity …
… R∀
says in particular that it “preserves all relations” (∀ (X Y : Type ℓ) → ∀ (Rel : X → Y → Type ℓ′) → …
) – once you define what that means for the stuff inside, of course
… and a function preserves relations when it takes related inputs to related outputs (R→
)
So, for the natural numbers of shape A -> (A -> A) -> A
, you get a relation R R→ (R R→ R) R→ R
(for any relation R
that relates two types)
… this means that when you prove two inputs z1, z2
are related by R
, and that the inputs s1, s2
take inputs related by R
to outputs also related by R
, you get that the outputs f _ z1 s1
and f _ z2 s2
are also related by R
.
And for the identity function A -> A
, it’s pretty simple: R R→ R
, any relation on the input is maintained for the output.
The proof that a particular function is parametric is pretty straightforward I think. Making use of it is a little trickier …
For a parametric function f .fst : ∀ A → (A → A)
with the shape of the identity function, the parametricity proof f .snd
simply says that that whatever relation the input satisfies, the output must also satisfy that relation …
… in particular, if the input is equal to a
, the output must also be equal to a
!
For something like f .fst : ∀ A → (A → A → A)
, you could pick a relation that says it is either equal to the first input or the second input.
Thus for finite types you can simply enumerate the possibilities, prove that the inputs each are one of the possibilities, and then know that the output is one of those possibilites (and so, one of the inputs).
But for f .fst : ∀ A → (A → (A → A) → A)
it is a little trickier, since we know this is equivalent to the natural numbers, which are infinite!
So what we do is we ask parametricity to tell us that what happens for an arbitrary element x : X
(of an arbitrary type) is in fact determined by what happens for some n : ℕ
of a known type.
We obtain a natural number that characterizes the function in the usual way – in particular, this is f .fst ℕ 0 (+1)
(roughly) – and then we “guess” what f .fst X z s
should be based on this natural number: itr n z s
, we just iteratively apply s
to z
, n
times.
In terms of relations, we say that a natural number n : ℕ
is related to the output x : X
when itr n z s ≡ x
.
And by definition of itr
, 0
is related to z
and when n
is related to x
then n+1
is related to s x
.
So parametricity tells us that itr (f .fst ℕ 0 (+1)) z s ≡ f .fst X z s
.
What happens at ℕ
determines what happens at any other type!
In particular, if f .fst ℕ = λ z s → z
(known type) then f .fst X = λ z s → z
(arbitrary type) as well, and so on for λ z s → s z
and λ z s → s (s z)
&c. by induction.
Thus f
does exactly what we expect, and there are no surprises lurking beneath the forall.
Updated to work with Agda version 2.6.1 and cubical library v0.2.