LiquidHaskell

Putting Things in Order

Hello again! Since we last met, much has happened that
we’re rather excited about, and which we promise to get
to in the fullness of time.

Today, however, lets continue with our exploration of
abstract refinements. We’ll see that this rather innocent
looking mechanism packs quite a punch, by showing how
it can encode various ordering properties of
recursive data structures.

but since we didn’t make the key type generic, it seems
we have no way to distinguish between the invariants of
the two sets of keys. Bummer!

Abstractly Refined Data

We could define two separate types of association
lists that capture different invariants, but frankly,
thats rather unfortunate, as we’d then have to
duplicate the code the manipulates the structures.
Of course, we’d like to have (type) systems help
keep an eye on different invariants, but we’d
really rather not have to duplicate code to
achieve that end. Thats the sort of thing that
drives a person to JavaScript ;-).

Fortunately, all is not lost.

If you were paying attention last time
then you’d realize that this is the perfect job for
an abstract refinement, this time applied to a data
definition:

163: {-@dataAssocv<p::Int->Prop>164: =KV(z::[(Int<p>,v)])@-}

The definition refines the type for Assoc to introduce
an abstract refinement p which is, informally speaking,
a property of Int. The definition states that each Int
in the association list in fact satisfies p as, Int<p>
is an abbreviation for {v:Int| (p v)}.

Now, we can have our Int keys and refine them too!
For example, we can write:

177: {-@digits::Assoc(String)<{\v -> (Btwn 0 v 9)}>@-}

to track the invariant for the digits map, and write

183: {-@sparseVec::AssocDouble<{\v -> (Btwn 0 v 1000)}>@-}

Thus, we can recover (some of) the benefits of abstracting
over the type of the key by instead parameterizing the type
directly over the possible invariants. We will have much
more to say on association lists
(or more generally, finite maps) and abstract refinements,
but lets move on for the moment.

Dependent Tuples

It is no accident that we have reused Haskell’s function
type syntax to define abstract refinements (p :: Int -> Prop);
interesting things start to happen if we use multiple parameters.

From the comments in Data.List, break p xs:
“returns a tuple where the first element is longest prefix (possibly empty)
xs of elements that do not satisfy p and second element is the
remainder of the list.”

We could formalize the notion of the second-element-being-the-remainder
using sizes. That is, we’d like to specify that the length of the second
element equals the length of xs minus the length of the first element.
That is, we need a way to allow the refinement of the second element to
depend on the value in the first refinement.
Again, we could define a special kind of tuple-of-lists-type that
has the above property baked in, but thats just not how we roll.

Instead, lets use abstract refinements to give us dependent tuples

225: data(a,b)<p::a->b->Prop>=(x:a,b<px>)

Here, the abstract refinement takes two parameters,
an a and a b. In the body of the tuple, the
first element is named x and we specify that
the second element satisfies the refinement p x,
i.e. a partial application of p with the first element.
In other words, the second element is a value of type
{v:b | (p x v)}.

As before, we can instantiate the p in different ways.
For example the whimsical

Abstractly Refined Lists

Right, we’ve been going on for a bit. Time to put things in order.

To recap: we’ve already seen one way to abstractly refine lists:
to recover a generic means of refining a monomorphic list
(e.g. the list of Int keys.) However, in that case we were
talking about individual keys.
Next, we build upon the dependent-tuples technique we just
saw to use abstract refinements to relate different
elements inside containers.

In particular, we can use them to specify that every pair
of elements inside the list is related according to some
abstract relation p. By instantiatingp appropriately,
we will be able to recover various forms of (dis) order.

The type is parameterized with a refinement p :: a -> a -> Prop
Think of p as a binary relation over the a values comprising
the list.

The empty list [] is a []<p>. Clearly, the empty list has no
elements whatsoever and so every pair is trivially, or rather,
vacuously related by p.

The cons constructor (:) takes a head h of type a and a tail
of a<p h> values, each of which is related tohand which
(recursively) are pairwise related [...]<p> and returns a list where
all elements are pairwise related [a]<p>.

Pairwise Related

Note that we’re being a bit sloppy when we say pairwise related.

What we really mean is that if a list

303: [x1,...,xn]::[a]<p>

then for each 1 <= i < j <= n we have (p xi xj).

To see why, consider the list

309: [x1,x2,x3,...]::[a]<p>

This list unfolds into a head and tail

313: x1::a314: [x2,x3,...]::[a<px1>]<p>

The above tail unfolds into

318: x2::a<px1>319: [x3,...]::[a<px1&&px2>]<p>

And finally into

323: x3::a<px1&&px2>324: [...]::[a<px1&&px2&&px3>]<p>

That is, each element xj satisfies the refinement
(p xi xj) for each i < j.

The first two cases are trivial: for an empty
or singleton list, we can vacuously instantiate
the abstract refinement with any concrete
refinement.

For the last case, we can inductively assume
mergeSort ys and mergeSort zs are sorted
lists, after which the type inferred for
merge kicks in, allowing LiquidHaskell to conclude
that the output is also sorted.

Quick Sort

The previous two were remarkable because they were, well, quite unremarkable.
Pretty much the standard textbook implementations work as is.
Unlike the classicaldevelopments
using indexed types we don’t have to define any auxiliary
types for increasing lists, or lists whose value is in a
particular range, or any specialized cons operators and
so on.

But, if you try it out, you’ll see that LiquidHaskell
does not approve. What could possibly be the trouble?

The problem lies with append. What type do we give ++?

We might try something like

495: (++)::IncrLista->IncrLista->IncrLista

but of course, this is bogus, as

499: [1,2,4]++[3,5,6]

is decidedly not an IncrList!

Instead, at this particular use of ++, there is
an extra nugget of information: there is a pivot
element x such that every element in the first
argument is less than x and every element in
the second argument is greater than x.

There is no way we can give the usual append ++
a type that reflects the above as there is no pivot
x to refer to. Thus, with a heavy heart, we must
write a specialized pivot-append that uses this fact:

Really Sorting Lists

The convenient thing about our encoding is that the
underlying datatype is plain Haskell lists.
This yields two very concrete benefits.
First, as mentioned before, we can manipulate
sorted lists with the same functions we’d use
for regular lists.
Second, by decoupling (or rather, parameterizing)
the relation or property or invariant from the actual
data structure we can plug in different invariants,
sometimes in the same program.

To see why this is useful, lets look at a real-world
sorting algorithm: the one used inside GHC’s
Data.Listmodule.

The interesting thing about the procedure is that it
generates some intermediate lists that are increasing
and others that are decreasing, and then somehow
miraculously whips this whirlygig into a single
increasing list.

Yet, to check this rather tricky algorithm with
LiquidHaskell we need merely write: