sum
and psum
mRTheorem
mRTheorem
on plusLast time we saw that we can use refinement types on the data type definitions to specify invariants about the data types. Today we will use this invariant to specify sortedness of lists.
Here’s a type for sequences that mimics the classical list:
The Haskell type above does not state that the elements are in order of course, but we can specify that requirement by refining every element in tl
to be greater than hd
:
Refined Data Constructors Once again, the refined data definition is internally converted into a “smart” refined data constructor
-- Generated Internal representation
data IncList a where
Emp :: IncList a
(:<) :: hd:a -> tl:IncList {v:a | hd <= v} -> IncList a
which ensures that we can only create legal ordered lists.
It’s all very well to specify ordered lists. Next, let’s see how it’s equally easy to establish these invariants by implementing several textbook sorting routines.
First, let’s implement insertion sort, which converts an ordinary list [a]
into an ordered list IncList a
.
The hard work is done by insert
which places an element into the correct position of a sorted list. LiquidHaskell infers that if you give insert
an element and a sorted list, it returns a sorted list.
Question: What should be the definition of iinsert
?
The function iinsert
can be defined as follows:
iinsert :: (Ord a) => a -> IncList a -> IncList a
iinsert y Emp = y :< Emp
iinsert y (x :< xs) | y <= x = y :< x :< xs
| otherwise = x :< iinsert y xs
Question: Can you update the definition of insertSort
to use foldr
?
The updated definition of insertSort
using foldr
is as follows:
insertSort :: (Ord a) => [a] -> IncList a
insertSort xs = foldr iinsert Emp xs
Question: What do you need to do to define insertion of decreasing lists?
First you need to define a data type for decreasing lists. Next, adjust the definition of the sorting function to construct the new type.
Ideally, we would like to write a verified sorted function that works for various sortedness properties and even over Haskell’s lists. Of course, we cannot constraint all lists to be sorted. But, we can abstract the notion of sortedness over the data declaration.
Thus, instead of explicitly stating that the head should be less that the element of the tail:
{-@ data IncList a =
Emp
| (:<) { hd :: a, tl :: IncList {v:a | hd <= v}} @-}
We say that there exists a predicate p
that relates the head and all elements of the tail:
Refined Data Constructors The internal data constructors are refined to be parametric with respect to the predicate p
:
-- Generated Internal representation
data PncList a where
Nil :: IncList a
Cons :: forall a, p. hd:a -> tl:PList <p> {v:a | p hd} -> PList <p> a
Now this abstract predicate can be instantiated with different properties:
Question: Give refinement types to the above four lists.
The list pl1
is increasing, pl2
is decreasing, pl3
is equal and pl4
is empty, which means that it could have any property.
{-@ pl1 :: IPList Int @-}
{-@ pl2 :: DPList Int @-}
{-@ pl3 :: EPList Int @-}
{-@ pl4 :: IPList Int @-}
So, now the same Haskell lists can be refined to be either decreasing, increasing or equal. For example, the code below inserts into an increasing list:
Question: Can you adjust it to insert
for decreasing lists?
You need to 1) change the type and 2) change the guard condition:
{-@ pinsert :: (Ord a) => a -> DPList a -> DPList a @-}
pinsert :: (Ord a) => a -> PList a -> PList a
pinsert y Nil = y `Cons` Nil
pinsert y (x `Cons` xs)
| y >= x = y `Cons` (x `Cons` xs)
| otherwise = x `Cons` pinsert y xs
Liquid Haskell comes by default with parametrized lists. So we can instantiate the refinements over Haskell’s lists directly.
With these definitions, we can verify insertion of elements into Haskell’s lists:
Question: Let’s also check if isort
preserves the len
of the list. (Where len
is the buildin measure for Haskell’s lists.)
The updated types that ensure the length preservation are the following:
{-@ insert :: (Ord a) => a -> x:IList a -> {v:IList a | len v = len x + 1 } @-}
{-@ isort :: (Ord a) => x:IList a -> {v:IList a | len x = len v} @-}
This abstraction, called Abstract Refinements is very powerful since it allows lists to turn from increasing to decreasing. Because of this flexibility, Liquid Haskell can automatically verify the below code, used as the official sorting function in Haskell, that very smartly sorts lists by collecting increasing and decreasing subsequences and merging them back together.
{-@ sort :: (Ord a) => [a] -> IList a @-}
sort :: (Ord a) => [a] -> [a]
sort xs = mergeAll (sequences xs)
where
sequences :: Ord a => [a] -> [[a]]
sequences (a:b:xs)
| a `compare` b == GT = descending b [a] xs
| otherwise = ascending b (a:) xs
sequences [x] = [[x]]
sequences [] = [[]]
descending :: Ord a => a -> [a] -> [a] -> [[a]]
descending a as (b:bs)
| a `compare` b == GT = descending b (a:as) bs
descending a as bs = (a:as): sequences bs
ascending :: Ord a => a -> ([a] -> [a]) -> [a] -> [[a]]
ascending a as (b:bs)
| a `compare` b /= GT = ascending b (\ys -> as (a:ys)) bs
ascending a as bs = as [a]: sequences bs
mergeAll [] = []
mergeAll [x] = x
mergeAll xs = mergeAll (mergePairs xs)
mergePairs :: Ord a => [[a]] -> [[a]]
mergePairs (a:b:xs) = merge1 a b: mergePairs xs
mergePairs [x] = [x]
mergePairs [] = []
merge1 :: Ord a => [a] -> [a] -> [a]
merge1 (a:as') (b:bs')
| a `compare` b == GT = b:merge1 (a:as') bs'
| otherwise = a:merge1 as' (b:bs')
merge1 [] bs = bs
merge1 as [] = as
Liquid Haskell has one more build in abstraction for pairs. So, internally the pairs are representing using abstract refinements as :
data Pair a b < p :: a -> b> = P {fst :: a, snd :: b < p fst> }
The generated type is also parametric:
-- Generated Internal representation
(,) :: forall a b <p :: a -> b>. x:a -> b <p x> -> Pair a b <p>
This allows us to specify dependent pairs, where the second element depends on the first:
So, now we can use the syntax of dependent type theory pairs to write interesting properties:
The above code says that for each natural number x
, there exists one y
that is greater than x
, taking us to a theorem proving territory, that we will return soon.
Question: Is this property true for less than too?
No, because it would encode that for every natural number, there exists a smaller natural number, which is not true. But, it is true for integers:
exLt :: Int -> (Int, ())
{-@ exLt :: x:Int -> (Int, ()) < {\f s -> f < x} > @-}
exLt x = (x-1, ())
We saw three ways to specify sortedness of lists:
The notion of abstract refinements is very powerful. In Liquid Haskell it is also used to encode dependent pairs and can be used in user defined data structures to specify various abstract dependencies.
Let’s implement and verify merge sort!
The implementation of merge sort is as follows:
{-@ mergeSort :: Ord a => [a] -> IList a @-}
mergeSort :: Ord a => [a] -> [a]
mergeSort [] = []
mergeSort [x] = [x]
mergeSort xs = merge (mergeSort xs1) (mergeSort xs2)
where (xs1, xs2) = splitAt (length xs `div` 2) xs
{-@ merge :: Ord a => IList a -> IList a -> IList a @-}
merge :: Ord a => [a] -> [a] -> [a]
merge [] ys = ys
merge xs [] = xs
merge (x:xs) (y:ys)
| x <= y = x:merge xs (y:ys)
| otherwise = y:merge (x:xs) ys