With dependent types, you can either capture function properties in the type signature, like in concatenation with length-indexed lists
(++) : Vect m a -> Vect n a -> Vect (m + n) a
or you can not use dependent types in your signature, like concatenation with standard lists
(++) : List a -> List a -> List a
and write proofs about (++)
appendAddsLength : (xs, ys : List a) -> length (xs ++ ys) = length xs + length ys
lengthNil : length [] = 0
lengthCons : (x : a) -> (xs : List a) -> length (x :: xs) = length xs + 1
Is there any difference between these approaches beyond ergonomics?
The most obvious difference is that, with
(++)onVects, the length is statically known: you can operate on it at compile time. Moreover, you don't need to write any additional proofs in order to ensure that(++)has the expected behavior onVects, whereas you need to do it forLists.That is,
(++)onVectis correct by construction. The compiler will always enforce the desired properties, whether you like it or not, and without the user taking any additional action.It's important to note that
length xsis not really interchangeable with the statically-known size in general. On these data types,lengthis a function which actually re-computes the length of aListorVectby walking through it and incrementing a counter:libs/prelude/Prelude/Types.idr:L403-L407libs/base/Data/Vect.idr:25-28Even with
Vect, the length built into to the type by construction, but the result of applying thelengthfunction to aListorVectis not fundamental at all. In fact,Data.Vectcontains a proof thatData.Vect.lengththat applyinglengthto aVect n talways returnsn:libs/base/Data/Vect.idr:30-34Using the above proof, we can assert statically, without actually executing
length, that the result oflengthis propositionally equal to the statically-known length of theVect. But this assurance is not available forList. And it's much more cumbersome to work with in general, likely requiring the use ofwith ... proofandrewritea lot more than just using the correct-by-construction type.