Refining types

Posted on October 5, 2016

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.