A Tactical System for Equations
We demonstrate the effectiveness containers as a framework for tactics by implementing tactics for a simple equational system with natural numbers as terms.
Numbers in this system can be built in three ways:
- We can directly build the number for
zero. - We can build the successor of a number given any other number.
- We can add two numbers together.
We represent those three cases via the constructors Zero, Succ and (+) for Term.
data Term : Type where
Zero : Term
Succ : Term -> Term
(+) : Term -> Term -> TermAn equality of terms is given by the following data with values for every way to prove that two numbers are equal:
data Eq : Term -> Term -> Type where- Every number is equal to itself.
Refl : Eq t t- Proofs of equalities are transitive, that is given and we get .
Trans : Eq r s -> Eq s t -> Eq r t- Proofs are congruent with regard to the
Succconstructor.
Cong : Eq t s -> Eq (Succ t) (Succ s)- Given two proofs of equality, their sum is also an equality.
CPlus : Eq t s -> Eq x y -> Eq (t + x) (s + y)- Proofs of equality are symmetric.
Sym : Eq t s -> Eq s t- Given three proofs of equality, their sum is associative.
Assoc : Eq t1 t2 -> Eq r1 r2 -> Eq s1 s2 ->
Eq ( t1 + (r1 + s1))
((t2 + r2) + s2)Primitive Tactics
With the above definition we can write the container that represents an equational problem. An equational problem has pairs of terms as questions and proofs of equality as responses.
EqProblem : Container
EqProblem = (t : Term * Term) !> uncurry Eq tUsing this container, we can build tactics that will attempt to resolve the problem, or break it down into subproblems. Starting easy, the sym tactic says that to solve a problem, it is enough to solve its symmetric.
sym : EqProblem =&> EqProblem
sym = !! \x => swap x ## SymThe above is an example of a tactic that always succeeds, it can always be applied to any problem. But most commonly, tactics can fail if the problem does not have the right shape. For this we use the Maybe monad on containers.
This is done for the refl tactic, this tactic attempts to apply the Refl proof to the problem, attempting to solve it immediately. Of course, this tactic only succeeds if both sides of the equations are the definitionally equal. We give the type for this tactic as a morphism into Maybe I
refl : EqProblem =&> Any.Maybe I
To implement this tactic we need a couple of helper functions, first we need a semi-decidable property for Term.
eq : (t, s : Term) -> Maybe (t === s)
eq Zero Zero = Just Refl
eq (Succ x) (Succ y) = map (\w => cong Succ w) (eq x y)
eq (x + y) (a + b) = do Refl <- eq x a
Refl <- eq y b
Just Refl
eq _ _ = NothingOnce the busywork is done we can write the tactic by running a test of equality on the
forward part, and then rebuild the proof in the backward part. Note that, thanks to the nature of the Maybe monad on containers, we only ever worry about the case where the two terms have a proof that they are equal.
refl : EqProblem =&> Any.Maybe I
refl = !! \x => case eq x.π1 x.π2 of
Nothing => Nothing ## absurd
Just p => Just () ## (\_ => rewrite p in Refl)The transitive property of equations can be used to build a tactic that will attempt to break down an equation into two equations and as long as is provided in advance. If the two subproblems and can be resolved, then we can build the larger proof via transitivity. This is a prime example of a tactic that breaks down a problem into two subproblems.
trans : (term : Term) -> EqProblem =&> (EqProblem ⊗ EqProblem)
trans term = !! \x => ((x.π1 && term) && (term && x.π2)) ## uncurry TransThe assoc tactic also breaks down an existing problem into subproblems but it can itself fail. This is because the associativity tactic can only work in a problem with the shape . If that is the case, then the problem can be resolved if we obtains proofs , and .
The type of this tactic is then:
assoc : EqProblem =&> Any.Maybe (EqProblem ⊗ (EqProblem ⊗ EqProblem))
assoc = !! \case ((a + (b + c)) && ((a' + b') + c')) =>
Just ((a && a') && (b && b') && (c && c')) ##
\(Aye (x1 && x2 && x3)) => Assoc x1 x2 x3
_ => Nothing ## absurdThe tactic matches on its argument to ensure it handle problems of the appropriate shape. Because associativity can only be applied to problem a + (b + c) we match
on the incoming problem and extract the values for a, b, and c. If the problem does not have the right shape, we return Nothing and the tactic fails.
User Defined Tactics
The rich monoidal structure on containers and their morphisms forms combinators that can be used to build larger tactics from smaller ones. For example, we can build a tactic that will attempt to solve the following equation, and only this one: , for all .
This is done by applying the symmetry tactic, then following it up by the associativity tactic. The associativity tactic generates three sub-problems, so we need to solve each one of them we do this by attempting to apply the reflexivity tactic, assuming that we are dealing with three variables in the same order but with a different bracketing.
The following program implements the above procedure:
assocCompose : EqProblem =&> (Any.Maybe I)
assocCompose
= sym
|&> assoc
|&> Closed.Maybe.Functor.Any.mapMaybe
((refl ~⊗~ (refl ~⊗~ refl))
|&> tripleRight combineUnits)
|&> Any.join {a = I}It might help to visualise the diagram that represents this computation
Draw the diagram here
The type of this lens indicates that it’s a leaf tactic and the Maybe monad shows that it
might fail to apply.