I donβt particularly like So or generally avoid justified verification conditions running in programs in general. This is more satisfactory to weave your expectations into the fabric of the data itself. I am going to write a type for "natural numbers less than n ".
data Fin : Nat -> Set where FZ : Fin (S n) FS : Fin n -> Fin (S n)
Fin is a numerical data type - compare the form of FS (FS FZ) with the form of the natural number S (SZ) , but with some additional level structure. Why is it called Fin ? There are exactly n unique members of type Fin n ; Fin , therefore, is a family of finite sets.
I mean: Fin really is a kind of number.
natToFin : (n : Nat) -> Fin (S n) natToFin Z = FZ natToFin (S k) = FS (natToFin k) finToNat : Fin n -> Nat finToNat FZ = Z finToNat (FS i) = S (finToNat i)
Here's the point: a Fin n value must be less than n .
two : Fin 3 two = FS (FS FZ) two' : Fin 4 two' = FS (FS FZ) badTwo : Fin 2 badTwo = FS (FS FZ) -- Type mismatch between Fin (S n) (Type of FZ) and Fin 0 (Expected type)
While we are in it, there are no numbers less than zero. That is, Fin Z , a set with a capacity of 0, is an empty set.
Uninhabited (Fin Z) where uninhabited FZ impossible uninhabited (FS _) impossible
If you have a number less than n , then it, of course, is less than S n . So we have a way to loosen the upper bound on a Fin :
weaken : Fin n -> Fin (S n) weaken FZ = FZ weaken (FS x) = FS (weaken x)
We can also go the other way, using the type checker, to automatically find the maximum possible binding to the given Fin .
strengthen : (i : Fin n) -> Fin (S (finToNat i)) strengthen FZ = FZ strengthen (FS x) = FS (strengthen x)
It is safe to determine the subtraction of Fin numbers from the number Nat , which are larger. We can also express the fact that the result will not be more than the input.
(-) : (n : Nat) -> Fin (S n) -> Fin (S n) n - FZ = natToFin n (S n) - (FS m) = weaken (n - m)
Everything works, but there is a problem: weaken works by rebuilding its argument in O (n) time, and we apply it with every recursive call - which gives O (n ^ 2) a subtraction algorithm. How awkward! weaken does exist to help type checking, but has a significant effect on the asymptotic complexity of the code. Can we leave without weakening the result of the recursive call?
Well, we had to call weaken , because every time we come across S , the difference between the result and the score increases. Instead of vigorously pulling the value to the correct type, we can close the gap by carefully pulling it down to meet it.
(-) : (n : Nat) -> (i : Fin (S n)) -> Fin (S (n `sub` finToNat i)) n - FZ = natToFin n (S n) - (FS i) = n - i
This type is inspired by our success in the compression of Fin associated with strengthen . Evaluation of the result - exactly the same as necessary.
sub that I used in the type is the subtraction of natural numbers. The fact that it is truncated by zero does not bother us, because type - ensures that it never happens. (This function can be found in Prelude as minus .)
sub : Nat -> Nat -> Nat sub n Z = n sub Z m = Z sub (S n) (S m) = sub nm
The lesson to be learned here is this. At first, the construction of some correctness properties in our data caused an asymptotic slowdown. We could refuse to create promises about the return value and return to the untyped version, but actually giving the type checking function more information allowed us to get to where we were going without sacrificing.