{-@ abz :: Int -> {v: Int | 0 < v } @-}
abz :: Int -> Int
abz n | 0 < n = n
| otherwise 0 - n
Here, we get an error saying that it can’t reconcile the types because the refinement we gave is 0 < v
not 0 <= v
{@- linsert :: Int -> lst:[Int] -> {v : [Int] | len v = len lst} / [len lst] @-}
linsert :: Int -> [Int] -> [Int]
linsert x [] = [x]
linsert y (x:xs)
| y < x = y:(x:xs)
| otherwise = x:(linsert y xs)
Here, we get a really long error message. It gives a list of things it has inferred, and tells you it could not prove the refinement you wanted.
Soundness: If it tells you it’s okay, then it is definitely okay.
Completeness: It can always find an answer.
Liquid Haskell is sound, but not necessarily complete, because sometimes it cannot find the answer on its own.
What if we wanted to sort lists of strings? Or lists of lists by their length?
We wouldn’t be able to use the sort and insert functions we currently have, because they expect type Int
But we wouldn’t want to write a separate sort function for every type of thing you want to sort.
Haskell has polymorphism, so we can do this:
linsert :: (Ord a) => a -> [a] -> [a]
lisort :: (Ord a) => [a] -> [a]
This says that for any type a
that is orderable, linsert
is a function that takes an a
and a list of a
and returns a list of a
.
There’s something really important about lists that we haven’t proven yet! That they actually sort the elements.
data SortedList a =
Mt
|Ln{h :: a, t :: SortedList a}
We define our own type for sorted lists. A sorted list is either empty (Mt
), or it’s a link (Ln
) between a head and a tail, where the head is type a
and the tail is type SortedList a
This definition doesn’t actually make sure the list is sorted yet. If the head of the list is 2
, for example, then we need to make sure that the tail of the list has values greater than or equal to 2
. We need to be able to say something about the values in the tail, given the value we have in the head.
How can we refine this with LiquidHaskell to make sure the list is sorted?
{@- data SortedList a =
Mt
| Ln{h :: a, t :: SortedList {v:a | v >= h}} @-}
Now, we have that the head is of type a
, and the tail is a sorted list of values that are greater than or equal to the value in the head. This recursively ensures that the whole list is sorted.
How can we use this type definition for our linsert
and lisort
functions?
{-@ linsert :: a -> lst: (SortedList a) -> SortedList a @-}
linsert :: (Ord a) => a -> SortedList a -> SortedList a
This says, linsert
takes an element, inserts it into a sorted list, and produces another sorted list.
Note that we took out the part of the refinement about the length of the list. len
doesn’t work for SortedList
because we never explained to Haskell what the length of a SortedList
means.
Why do we need the LiquidHaskell refinement as well as the Haskell type annotations? Because the Haskell type SortedList
doesn’t actually make sure the list is sorted. So Haskell will just check if the values follow the recursive data defintion we made for SortedList
(which is just any linked list)
linsert x Mt = (Ln x Mt)
linsert y (Ln x xs)
| y < x = (Ln y (Ln x xs))
| otherwise = (Ln x (linsert y xs))
Okay now let’s change the types for lisort
:
{-@ lisort :: lst:(SortedList a) -> {v : SortedList a | len v = len lst} @-}
lisort :: (Ord a) => SortedList a -> SortedList a
Wait, is that correct? That says, take a sorted list and produce a sorted list. But, that’s kind of a useless sorting algorithm, right?
What do we want instead?
{-@ lisort :: lst:[a] -> {v : SortedList a | len v = len lst} @-}
lisort :: (Ord a) => [a] -> SortedList a
lisort [] = Mt
lisort x:xs = linsert x (lisort xs)
Okay, now we get an error from LiquidHaskell that len
isn’t defined for SortedList
. So let’s define a measure for the length of a SortedList
{-@
measure sortedlen :: SortedList a -> Int
sortedlen Mt = 0
sortedlen (Ln h t) = 1 + (sortedlen t)
@-}
Then we can modify our lisort
refinement to:
{-@ lisort :: lst:[a] -> {v : SortedList a | sortedlen v = len lst} @-}
This solves one error, but we still have the termination error. Let’s turn off termination checking:
{-@ LIQUID "no-termination" -@}
Ok, we still get a type error. This is because it can’t infer anything about the length of the output list from linsert
. So let’s add back in the stuff about the length of the list in the refinement for linsert
{-@
linsert :: a -> len:(SortedList a) -> {v : SordedList a | sortedlen v = sortedlen lst} / [sortedlen lst]
@-}
Okay great! Now it’s safe (with termination still turned off).
To get termination to work, you need to add an invariant that every SortedList
has a length greater than or equal to 0.
{-@ invariant {v: SortedList a | sortedlen v >= 0} @-}
Yay! Now everything is safe :)