Sortedness & Abstract Refinements

{- OPTIONS_GHC -fplugin=LiquidHaskell #-} {-@ LIQUID "--no-termination" @-} module Lecture_03_Sortedness where main :: IO () main = return ()

Ordered Lists

Last 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:

data IncList a = Emp | (:<) { hd :: a, tl :: IncList a } infixr 9 :<

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:

{-@ data IncList a = Emp | (:<) { hd :: a, tl :: IncList {v:a | hd <= v}} @-}


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.

okList :: IncList Int okList = 1 :< 2 :< 3 :< Emp -- accepted by LH badList :: IncList Int badList = 2 :< 1 :< 3 :< Emp -- rejected by LH

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.

insertSort :: (Ord a) => [a] -> IncList a insertSort [] = Emp insertSort (x:xs) = iinsert x (insertSort xs)

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.

iinsert :: (Ord a) => a -> IncList a -> IncList a iinsert y Emp = y :< Emp iinsert y (x :< xs) = undefined

Question: What should be the definition of insert?

Abstraction over Sortedness

Ideally, we would like to write a verified sorted function that works 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:

data PList a = Nil | Cons a (PList a) {-@ data PList a < p :: a -> a -> Bool> = Nil | Cons { phd :: a, ptl :: PList < p > a < p phd > } @-}

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:

{-@ type IPList a = PList <{\hd v -> hd <= v}> a @-} {-@ type DPList a = PList <{\hd v -> hd >= v}> a @-} {-@ type EPList a = PList <{\hd v -> hd == v}> a @-} pl1, pl2, pl3, pl4 :: PList Int pl1 = Cons 1 (Cons 2 (Cons 3 Nil)) pl2 = Cons 3 (Cons 2 (Cons 1 Nil)) pl3 = Cons 1 (Cons 1 (Cons 1 Nil)) pl4 = Nil

Question: Give refinement types to the above four lists.

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:

{-@ pinsert :: (Ord a) => a -> IPList a -> IPList 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

Question: Can you adjust it to insert fointor decreasing lists?

Haskell’s Lists

Liquid Haskell comes by default with parametrized lists. So we can instantiate the refinements over Haskell’s lists directly.

{-@ type IList a = [a]<{\hd v -> (v >= hd)}> @-} {-@ type DList a = [a]<{\hd v -> (v <= hd)}> @-} {-@ type EList a = [a]<{\hd v -> (v == hd)}> @-} ilist, dlist, elist :: [Int] {-@ ilist :: IList Int @-} ilist = [1, 2, 3] {-@ dlist :: DList Int @-} dlist = [3, 2, 1] {-@ elist :: EList Int @-} elist = [1, 1, 1]

With these definitions, we can verify insertion of elements into Haskell’s lists:

{-@ insert :: (Ord a) => a -> IList a -> IList a @-} insert :: (Ord a) => a -> [a] -> [a] insert y [] = [y] insert y (x:xs) | y <= x = y:x:xs | otherwise = x:insert y xs {-@ isort :: (Ord a) => IList a -> IList a @-} isort :: (Ord a) => [a] -> [a] isort [] = [] isort (x:xs) = insert x (isort xs)

Question: Let’s also check if isort preserves the len of the list.

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

Dependent Pairs

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:

{-@ pair1 :: (Int, Int) < {\f s -> f <= s} > @-} pair1 :: (Int, Int) pair1 = (2, 4)

So, now we can use the syntax of dependent type theory pairs to write interesting properties:

exGt :: Int -> (Int, ()) {-@ exGt :: x:Nat -> (Nat, ()) < {\f s -> f > x} > @-} exGt x = (x+1, ())

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?

Summary

We saw three ways to specify sortedness of lists:

  1. By refining the data type definition,
  2. By abstracting the sortedness property, and
  3. By refining the list type directly.

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.