gdp-0.0.0.1: src/Logic/Proof.hs
{-# LANGUAGE KindSignatures #-}
{-|
Module : Logic.Proof
Copyright : (c) Matt Noonan 2018
License : BSD-style
Maintainer : matt.noonan@gmail.com
Portability : portable
-}
module Logic.Proof
( -- * The `Proof` monad
Proof
, (|.), (|$), (|/), (|\), ($$)
, given
, axiom, sorry
) where
import Data.Coerce
import Control.Monad ((>=>))
{--------------------------------------------------
The `Proof` monad
--------------------------------------------------}
{-| The @Proof@ monad is used as a domain-specific
language for constructing proofs. A value of type
@Proof p@ represents a proof of the proposition @p@.
For example, this function constructions a proof of
"P or Q" from the assumption "P and Q":
> and2or :: (p && q) -> Proof (p || q)
>
> and2or pq = do
> p <- and_elimL pq -- or: "p <- fst <$> and_elim pq"
> or_introL p
If the body of the proof does not match the proposition
you claim to be proving, the compiler will raise a type
error. Here, we accidentally try to use @or_introR@
instead of @or_introL@:
> and2or :: (p && q) -> Proof (p || q)
>
> and2or pq = do
> p <- and_elimL pq
> or_introR p
resulting in the error
@
• Couldn't match type ‘p’ with ‘q’
‘p’ is a rigid type variable bound by
the type signature for:
and2or :: forall p q. (p && q) -> Proof (p || q)
‘q’ is a rigid type variable bound by
the type signature for:
and2or :: forall p q. (p && q) -> Proof (p || q)
Expected type: Proof (p || q)
Actual type: Proof (p || p)
@
-}
data Proof (pf :: *) = QED
instance Functor Proof where
fmap _ = const QED -- modus ponens (external?)
instance Applicative Proof where
pure = const QED -- axiom
pf1 <*> pf2 = QED -- modus ponens (internal?)
instance Monad Proof where
pf >>= f = QED
{-| This operator is just a specialization of @(>>=)@, but
can be used to create nicely delineated chains of
derivations within a larger proof. The first statement
in the chain should produce a formula; @(|$)@ then
pipes this formula into the following derivation rule.
> and2or :: (p && q) -> Proof (p || q)
>
> and2or pq = and_elimL pq
> |$ or_introL
-}
(|$) :: Proof p -> (p -> Proof q) -> Proof q
(|$) = (>>=)
infixr 7 |$
--(|-) :: ((p -> Proof r) -> Proof r) -> (p -> Proof r) -> Proof r
{-| This operator is used to create nicely delineated chains of
derivations within a larger proof. If X and Y are two
deduction rules, each with a single premise and a single
conclusion, and the premise of Y matches the conclusion of X,
then @X |. Y@ represents the composition of the two
deduction rules.
> and2or :: (p && q) -> Proof (p || q)
>
> and2or = and_elimL
> |. or_introL
-}
(|.) :: (p -> Proof q) -> (q -> Proof r) -> p -> Proof r
(|.) = (>=>)
infixr 9 |.
{-| The @(|/)@ operator is used to feed the remainder of the proof in
as a premise of the first argument.
A common use-case is with the @Or@-elimination rules @or_elimL@ and
@or_elimR@, when one case is trivial. For example, suppose we wanted
to prove that @R && (P or (Q and (Q implies P)))@ implies @P@:
@
my_proof :: r && (p || (q && (q --> p))) -> Proof p
my_proof =
do and_elimR -- Forget the irrelevant r.
|. or_elimL given -- The first case of the || is immediate;
|/ and_elim -- The rest of the proof handles the second case,
|. uncurry impl_elim -- by unpacking the && and feeding the q into
-- the implication (q --> p).
@
The rising @/@ is meant to suggest the bottom half of the proof getting
plugged in to the Or-elimination line.
-}
(|/) :: (p -> (q -> Proof r) -> Proof r) -> (q -> Proof r) -> p -> Proof r
(|/) = flip
infixr 9 |/
{-| The @(|\\)@ operator is used to take the subproof so far and feed it
into a rule that is expecting a subproof as a premise.
A common use-case is with the @Not@-introduction rule @not_intro@,
which has a type that fits the second argument of @(|\\)@. By way
of example, here is a proof that "P implies Q" along with "Not Q"
implies "Not P".
@
my_proof :: (p --> q) -> (Not p --> r) -> Not q -> Proof r
my_proof impl1 impl2 q' =
do modus_ponens impl1 -- This line, composed with the next,
|. contradicts' q' -- form a proof of FALSE from p.
|\\ not_intro -- Closing the subproof above, conclude not-p.
|. modus_ponens impl2 -- Now apply the second implication to conclude r.
@
The falling @\\@ is meant to suggest the upper half of the proof
being closed off by the Not-introduction line.
-}
(|\) :: (p -> Proof q) -> ((p -> Proof q) -> Proof r) -> Proof r
(|\) = flip ($)
infixl 8 |\
-- | Take a proof of @p@ and feed it in as the first premise of
-- an argument that expects a @p@ and a @q@.
($$) :: (p -> q -> Proof r) -> Proof p -> (q -> Proof r)
(f $$ pp) q = do { p <- pp; f p q }
-- | @given@ creates a proof of P, given P as
-- an assumption.
--
-- @given@ is just a specialization of @pure@ / @return@.
given :: p -> Proof p
given _ = QED
-- | @sorry@ can be used to provide a "proof" of
-- any proposition, by simply assering it as
-- true. This is useful for stubbing out portions
-- of a proof as you work on it, but subverts
-- the entire proof system.
--
-- _Completed proofs should never use @sorry@!_
sorry :: Proof p
sorry = QED
{-| @axiom@, like @sorry@, provides a "proof" of any
proposition. Unlike @sorry@, which is used to indicate
that a proof is still in progress, @axiom@ is meant to
be used by library authors to assert axioms about how
their library works. For example:
@
data Reverse xs = Reverse Defn
data Length xs = Length Defn
rev_length_lemma :: Proof (Length (Reverse xs) == Length xs)
rev_length_lemma = axiom
@
-}
axiom :: Proof p
axiom = QED