In dependently typed programming, I find it’s common to start with some “imprecise” type and refine it to something that contains more information. For example, I might receive a String
from user input and want to turn that into a Vect n Char
(for some given n
) when I write proofs about my code:
fromString : String
-> (n : Nat ** Vect n Char)
Generally this refinement is accompanied by proofs that the refinement was correct. For instance, we might want to show that length is preserved:
lengthPreserved : (s : String)
-> getWitness (fromString s) = length s
and use this to build a copy of the refined value with the existential unpacked:
fromString' : (s : String) -> Vect (length s) Char
fromString' s = let prf = lengthPreserved s in
rewrite sym prf in
getProof $ fromString s
Exercise: Implement fromString
and lengthPreserved
.
This factorization of fromString'
into the simpler components of lengthPreserved
and fromString
makes you wonder if this process can be generalized. To start off this generalization, we are looking for a function that takes length
and the two components and produces fromString'
:
fromString' : (length : String -> Nat)
-> (fromString : String -> (n : Nat ** Vect n Char))
-> (lengthPreserved : (s : String) -> getWitness (fromString s) = length s)
-> (s : String)
-> Vect (length s) Char
Generalizing the dependent pair to the Sigma
typeclass (and making the substitution Vect' n = Vect n Char
), we get:
fromString' : (length : String -> Nat)
-> (fromString : String -> Sigma Nat Vect')
-> (lengthPreserved : (s : String) -> getWitness (fromString s) = length s)
-> (s : String)
-> Vect' (length s)
and we’re basically there. Now, all that’s left is to parameterize on our unrefined type String
, our refined type Vect'
, and its index the Nat
(along with some generous renaming and re-arranging):
sko : {a b : Type}
-> { pred : b -> Type }
-> (iexists : a -> Sigma b pred)
-> (f : a -> b)
-> (witness : (s : a) -> getWitness (iexists s) = f s)
-> (s : a)
-> p (f s)
Now, to figure out what this thing really is, let’s turn to the Curry-Howard interpretation of it, going parameter by parameter:
iexists : a -> Sigma b t
If we think of a
as an index, this describes an indexed family of existence proofs on the predicate pred
, with witnesses taking type a
.
f : a -> b
f
tells us that a
can be interpreted as something other than an index, by relating it to b
.
(witness : (s : a) -> getWitness (iexists s) = f s)
Now, f
has been related to the existence proofs by being identical to the witness over a
. This tells us that f
perfectly describes the witnesses of the indexed family iexists
.
(s : a) -> p (f s)
Given all these conditions, we can think of this as a new family of indexed proofs, except the witness has been replaced with a transformation of f
. The existential type has been eliminated and replaced with a universally quantified type instead.
Now, whenever we want to prove that a refinement into another type has a certain property, we don’t have to do it all in one go. We also don’t have to worry about unpacking existentials afterwards. Curiously, this process looks a lot like skolemization.