sum
and psum
mRTheorem
mRTheorem
on plusEach time you write a program, you are actually writing a proof.
What are the programs prooving? The Curry-Howard correspondence, independently developed by Curry (1934) and Howard (1969), tells us that programs are proofs of theorems.
In this and the next lectures we will explore this correspondence in more detail and see how to use it to both write correct programs and prove theorems.
For example, an identity function on integers, idInt
, is a proof that for all integers x
, idInt x
is also an integer.
The standard identity polymorphic function, id
, is a proof that for all types a
, id x
is also of type a
.
Polymorphism is very commonly used in implicitely proving theorems about the programs, as described in theorems for free.
Let’s now try to to define a function that states that for each integer, there exists an a
:
Question: Can you define the function above?
The only way to define the above function is via divergence. Thus, note that when a polymorphic type appears only in the result of your function, then most probably your function is not terminating… The property that it is “proving” does not hold.
Programs as proofs when they are well formed meaning, they are terminating and total.
In Liquid Haskell, we can write propositions as refinement types. Concretely, we use refinement types to express theorems and define their Haskell functions as proofs of these theorems.
For example, below we prove that 1 + 1 = 2
by defining a function onePlusOne
The result of the function onePlusOne
is just a unit. Thus the function has no computational content. But, its result type is define to state a theorem. The proof of the theorem is performed just by the SMT solver that knows linear arithmetic.
Liquid Haskell come with the proof combinators library that allows to make the proofs more readable.
As a first simplification step, Liquid Haskell allows to abbreviate the type {v:T | p }
into just {p}
, when v
is not used in the type.
As a second simplification, the proof combinators library defines various functions that allow to make the proofs more readable. For example, it define the following proof combinators:
-- Defined at Language.Haskell.Liquid.ProofCombinators
type Proof = () -- Proof is just a unit
trivial :: Proof
trivial = ()
data QED = QED
(***) :: a -> QED -> Proof
_ *** _ = ()
With these, we can put the famous
QED
: quod erat demonstrandum (which was to be demonstrated)
at the end of the proof.
Thus, using the Proof Combinator library, in Liquid Haskell, we can prove, for free theorems that the SMT knows how to prove.
Question: Can you name another trivial theorem that you can prove using Liquid Haskell?
Quantified theorems (e.g., \(\forall x . \dots\)) can also be expressed in Liquid Haskell, by using functional arguments as the universal quantifiers.
For example, we can prove that addition is commutative:
Note, function arguments work as universal quantifiers, and also, due to currin, we use function abstraction to express existential quantifiers.
Next, we will see how we can use Liquid Haskell to prove theorems about Haskell functions.
To do so, first, we need to turn on the reflection
flag of Liquid Haskell.
This flag let us reflect
Haskell functions into the predicate logic of Liquid Haskell. For example, we can reflect the fibonacci
function as follows:
The reflect
annotation:
fib
that can be used in the predicates of Liquid Haskell.fib
that exactly captures the function definition.
{-@ fib :: n:Nat -> {v:Nat | v = fib n &&
v = if n == 0 then 0
else if n == 1 then 1
else fib (n-1) + fib (n-2) } @-}
Thus, now we can prove properties about the fib
function.
where ===
is a proof combinator function used for equational reasoning (in the style of Dafny’s calculations). It’s type first checks that the first two arguments are equal and then returns the second, to keep working on the proof.
(===) :: x:a -> y:{a| x = y} -> {v:a | v = y }
(=<=) :: x:a -> y:{a | x <= y} -> {v:a | v == y}
(=>=) :: x:a -> y:{a | x >= y} -> {v:a | v == y}
Similarly, the =<=
and =>=
proof combinators are used for inequalities.
Let’s now prove that fib 3 = 2
:
Question: Can you complete the proof above?
If you have completed the proof you might have duplicated the proof of fibTwo
. In Liquid Haskell, we can reuse proofs by using the because
combinator. The proof combinators library defines the because
combinator as follows:
(?) :: a -> b -> a
x ? _ = x
Thus, essentially introducing the it’s second argument as a fact in the proof.
Question: Let’s now complete the proof of fibThree
using the because
combinator.
Now that we have introduced all the vocabulary of the proof combinators library, let’s prove a more interesting theorem about the fib
function. Let’s prove that fib
is increasing.
Question: Can you complete the proof above?
To simplify proofs, Liquid Haskell has a tactic, called ple
(Proof by Logical Equivalence) that automates most equational reasoning steps, but still requires the case splitting and the lemma invocations.
ple
flag, we can simplify the proof of fibUp
.
Let’s now prove that the Fibonacci function is monotonic:
Question: Can you complete the proof above? Concretely, complete the missing calls to the fibUp
lemma and the inductive hypothesis and the termination metric of the proof.
Looking for closely at the monotonicity proof of the Fibonacci function, we can see that the proof is not actually using the definition of the Fibonacci function, but only the fact that it is increasing. Thus, we can turn the proof into a generic proof of the monotonicity of any function f
that is increasing.
Question: Can you complete the proof above?
Once we have the general (a.k.a. higher-order) proof that increasing functions are monotonic, we can use it to prove the monotonicity of the Fibonacci function.
The proofs we did so far are essentially proofs by natural induction. Let’s prove the textbook theorem that the sum of the first n
natural numbers is n*(n+1)/2
.
For that, we first define the sumTo
function that computes the sum of the first n
natural numbers.
Next, we prove that the sumTo
function is computing the sum of the first n
natural numbers.
Question: Can you complete the proof above? Hint, the Haskell function div
is the integer division.
Next, we will see how the concept of natural induction can be generalized to prove properties of data types as structural induction.
In this lecture we have seen how to use Liquid Haskell to prove theorems about Haskell functions. We have seen how to use the proof combinators library to make the proofs more readable, by expressing equational reasoning steps and the because operator. We saw that using the reflection
flag we can turn Haskell functions into predicates of Liquid Haskell and the ple
flag we can simplify proofs. Finally, we saw that proofs are higher-order functions that can be reused to prove other theorems.