This is the second of a series on Hillel Wayne's Great Theorem Prover Showdown. Make sure to check out the first part! You can also follow my progress in this repository.
In the last installation of "Cam learns to use a theorem prover", we got acquainted with Liquid Haskell, induction, and formal specifications while writing a verified version of the legendary LeftPad function.
Today, I'll be going over my solution to the next challenge, "Unique". This one actually gave me more trouble than LeftPad, contrary to Twitter's opinion of it.
Our task is to, given a list L
construct a list
containing every unique element of L
exactly once (in
any order).
Let's start with the code itself:
{-@ reflect elem @-}
elem :: Eq a => a -> [a] -> Bool
elem x [] = False
elem x (y:ys) = x == y || elem x ys
{-@ reflect unique @-}
unique :: Eq a => [a] -> [a]
= []
unique [] :xs)
unique (x| x `elem` xs = unique xs
| otherwise = x:unique xs
This is fairly straightforward -- an element x
is
unique in x:xs
if x
is not present in
xs
, so we keep it. Now for the specification:
Takes a sequence of integers, returns the unique elements of that list. There is no requirement on the ordering of the returned values.
As usual, I think that our code is "obviously" correct. In
fact, our code is even more general -- we can take any
Eq a
, but it specializes to integers easily
enough.
Let's see if we can prove it.
The "Obvious" Part
The "main" property of Unique
is that every
element in xs
appears exactly once in
unique xs
. And this is important, because it means
that a) we didn't drop any elements and b) we actually removed
duplicate elements. We can do so like this:
{-@ reflect count @-}
count :: Eq a => a -> [a] -> Int
= 0
count _ [] :ys)
count x (y| x == y = 1 + count x ys
| otherwise = count x ys
{-@ thmUniqueNoDup :: xs:[a] -> {x:a | elem x xs} ->
{count x (unique xs) == 1}
@-}
thmUniqueNoDup :: Eq a => [a] -> a -> ()
Now, here is where I got stuck. No matter what I tried, I couldn't quite come up with a way to make LH infer this proof automagically, the way we did before.
It turns out that LH does come with some hand-guidance tools. We can, in fact, step through code execution symbolically to show that it exhibits some property (correctness or otherwise) for all relevant inputs. While automating the entire process can be computationally intractable, we (as humans) have a fair bit more intuition and can "know" where to begin -- we can write the proof, and the computer can check if our proof is correct1.
Here's the case where y
is an element of
xs
.
thmUniqueNoDup :: Eq a => [a] -> a -> Proof
:xs) y
thmUniqueNoDup (x| y `elem` xs =
if x == y then
:xs))
count y (unique (x==. count y (unique xs)
==. 1 ? thmUniqueNoDup xs y
*** QED
else
:xs))
count y (unique (x==. count y (x:unique xs)
==. count y (unique xs)
==. 1 ? thmUniqueNoDup xs y
*** QED
Let's break this down a bit. ==.
is the Liquid
Haskell combinator for "extensionally equivalent to", which means
that the expressions on both sides will evaluate to the same value
(or lack of value). When we use this, we're asking LH to check
whether using that equivalence as a step is justified2.
As before, though, sometimes LH can't figure out that a given
step is justified on its own. Instead, we can provide a
justification for a step with the ?
combinator. For
example, in this step
==. 1 ? thmUniqueNoDup xs y count y (unique xs)
By our assumption on y
, we know
y ``elem`` xs
. Thus, by the types of
thmUniqueNoDup
(that is, by our inductive
hypothesis), we know that count y (unique xs)
evaluates to 1
.
The final thing here is *** QED
, which finalizes
the "proof" in Haskell types. We need this to ensure that we can
actually have a term of the Haskell type Proof
provided by Liquid Haskell.
Next case might start looking like this:
thmUniqueNoDup :: Eq a => [a] -> a -> Proof
:xs) y
thmUniqueNoDup (x| otherwise =
:xs))
count y (unique (x==. 1 + count y (unique xs)
==. 1 ? someTheorem
*** QED
So what can we use for someTheorem
? We'd need to
somehow show that count y (unique xs)
is
0
, but our inductive hypothesis can only tell us that
count x (unique xs)
is equal to 1
!
It turns out that we don't actually have enough to show that the other case is sound. To do that, we'll need two auxiliary theorems:
{-@ reflect thmElemCount @-}
{-@ thmElemCount :: Eq a =>
x:a -> xs:{[a] | not elem x xs} -> { count x xs == 0 }
@-}
thmElemCount :: Eq a => a -> [a] -> ()
= ()
thmElemCount x [] :ys) = thmElemCount x ys
thmElemCount x (y
{-@ thmUniqueExistInv ::
xs:[a] ->
{x:a | not elem x xs} ->
{ not elem x (unique xs) }
@-}
thmUniqueExistInv :: Eq a => [a] -> a -> Proof
= trivial *** QED
thmUniqueExistInv [] y :xs) y
thmUniqueExistInv (x= elem y (unique (x:xs))
==. elem y (x:unique xs)
==. elem y (unique xs)
==. False ? thmUniqueExistInv xs y
*** QED
The first simply creates and proves the "obvious" relation
between elem
and count
. We'll ultimately
be invoking this theorem to get our 0
. The
second is slightly more interesting. Note that the precondition
for thmElemCount
is that x
is
not an element of xs
. However, we haven't
actually shown that unique xs
won't contain any
elements not in xs
! So we need to prove it.
This gives us our final step:
...
==. 1 + count y (unique xs)
==. 1 ? thmUniqueExistInv xs y `seq` thmElemCount y (unique xs)
*** QED
Notice the seq
here. In order to cite
thmElemCount
, we need to show that y
is
not an element of unique xs
, so we must first use
thmUniqueExistInv
.
The Less Obvious Part
In most worlds, this would be enough. We've shown that, for
every element in xs
, unique xs
contains
that element exactly once. Incidentally, we also showed that
unique xs
didn't "spawn" elements -- if an element
isn't in xs
, it can't be in unique xs
either.
However, we're not done yet. The world of type theory (of which
Liquid Haskell's refinement types are a subworld) is one based on
constructive logic (otherwise known as intuitionistic
logic), which removes the law of the excluded middle. That
is, \(\neg \neg P \nRightarrow
P\). In this particular case, that means that we're not
done! We've only proven that every element in
unique xs
is not not in xs
,
which is not the same as saying that it's in
xs
3.
For the sake of completeness, let's show that x
being in unique xs
implies that x
is in
xs
as well!
{-@ thmUniqueExist ::
xs:[a] ->
{x:a | elem x (unique xs)} ->
{ elem x xs }
@-}
thmUniqueExist :: Eq a => [a] -> a -> Proof
:xs) y
thmUniqueExist (x| y == x = trivial *** QED
| otherwise
= elem y (x:xs)
==. elem y xs
==. True ? thmUniqueExist' (x:xs) y `seq` thmUniqueExist xs y
*** QED
{-@ thmUniqueExist' ::
xs:{[a] | len xs > 0} ->
{x:a | elem x (unique xs) && x != hd xs} ->
{ elem x (unique (tl xs)) }
@-}
thmUniqueExist' :: Eq a => [a] -> a -> Proof
:xs) y
thmUniqueExist' (x= elem y (unique (tl (x:xs)))
==. elem y (unique xs)
==. elem y (x:unique xs)
==. elem y (unique (x:xs))
==. True
*** QED
I won't go over every step, but an important step is in the
derivation of thmUniqueExist'
, in which we actually
step "backwards" from elem y (x:unique xs)
to
elem y (unique (x:xs))
(by the definition of
unique
). I'm actually not particularly satisfied with
this proof; in particular I find the forced use of hd
and tl
to be extremely inelegant. If you know of a
better way, please let me know!
Onwards to Fulcrum! This one may take a bit longer -- I've been sitting on this solution to Unique for about a week, but Fulcrum has been giving me way more trouble.
If you think this sounds similar to P vs NP, you'd be correct! Proof generation is actually coNP-complete, which means that it's easy to check that a proof is wrong, but difficult to create a proof from scratch or verify that it's correct!↩︎
Technically, LH doesn't check every step. The official documentation states that LH will only check that the first and last steps are justified, but with some experimentation I've found that certain intermediate steps are mandatory. Who knows?↩︎
The difference may seem academic to you, and in this case it arguably is. For a better discussion of the difference between classical and logical systems, check out this awesome blog post by Professor Harper of CMU.↩︎