%{
Logics can be defined in Twelf in such a way that it may not be possible
to do proof search by the fixed search strategy of Twelf's
[[logic programming]] engine. In these cases, tactical theorem provers can
be written that may still be able to prove many theorems. This article
defines two approaches to writing these tactical theorem provers.
}%
%{ == Logic definition == }%
%{
When we introduce numbers with addition on this wiki (see for example
[[natural numbers]]), we usually define a judgmental definition of addition
and then prove that it has the properties we desire. This will be a rather
different presentation, more in line with the [[Zermelo Frankel]] case study.
This is a signature for an object language (a logic) that has addition
as a primitive operation.
We define a type for numbers num, and then make addition a
primitive operation. We also define propositions; the only proposition we
define here is equality of two numbers.
}%
num : type. %name num N.
+ : num -> num -> num. %infix left 10 +.
0 : num.
1 : num.
prop : type.
== : num -> num -> prop. %infix none 5 ==.
%{
We can create a valid proposition that may be obviously untrue; for instance,
(0 == 1) is a valid object of type prop. Therefore,
we create pf, which is a particular little logic which
will allow us to prove a large number of theorems about addition based
on a small number of axioms. Eight axioms are defined below:
}%
pf : prop -> type.
refl : pf (N == N).
symm : pf (N1 == N2) -> pf (N2 == N1).
trans : pf (N1 == N2) -> pf (N2 == N3) -> pf (N1 == N3).
plus_assoc : pf (N1 + N2 + N3 == N1 + (N2 + N3)).
plus_comm : pf (N1 + N2 == N2 + N1).
plus_zero : pf (N1 + 0 == N1).
plus_cong : pf (N1 == N1') -> pf (N2 == N2') -> pf (N1 + N2 == N1' + N2').
plus_elim1 : pf (N1 + N2 == N1 + N2') -> pf (N2 == N2').
%freeze pf.
%{
We [[freeze]] the type family pf to prevent any more
axioms from being defined,
but we can still define (many!) more theorems using the axioms (for instance,
the complement to plus_elim1.
}%
plus_elim2 : pf (N1 + N2 == N1' + N2) -> pf (N1 == N1') =
[p1 : pf (N1 + N2 == N1' + N2)]
plus_elim1 (trans (trans plus_comm p1) plus_comm).
%{ == Tactical theorem proving == }%
%{ === Motivation: "flattening" a numeric formula === }%
%{
Say we want to define a predicate mklist that takes some numeric
formula num and applies associativity exhaustively to "flatten"
the formula into a list, for instance transforming (a + (b + c) + d)
into (a + b + c + d) - addition was defined to be left-associative,
so this is the same as (((a + b) + c) + d).
The list type family below will do this when run as a logic program.
}%
list : num -> num -> type.
%mode list +A -B.
list-swap : list (A + (B + C)) D
<- list (A + B + C) D.
list-step : list (A + C) (B + C)
<- list A B.
list-stop : list A A.
%{ === Returning a proof of equality === }%
%{
The operation of list may seem a bit mysterious - how
do we know that the straightened out formula is equal to the old one? By using
Twelf's [[dependent types]], we can define a new type family mklist
which operates in the same way but returns a proof that the two numeric
formulas are equal. We do '''not''' prove that the second
formula is flattened, just that it is equal to the first formula.
}%
mklist : {A}{B} pf (A == B) -> type.
%mode mklist +A -B -Pf.
mklist-swap : mklist (A + (B + C)) D (trans (symm plus_assoc) Pf)
<- mklist (A + B + C) D (Pf: pf (A + B + C == D)).
mklist-step : mklist (A + C) (B + C) (plus_cong Pf refl)
<- mklist A B (Pf: pf (A == B)).
mklist-stop : mklist A A refl.
%{ === Using the tactical theorem prover === }%
%{
We can then use [[Define declaration|%define]] and
[[%solve]] to create
a proof that (a + (b + c) + d == a + b + c + d). In order for mklist
to terminate, it must be given a [[ground]] term, so we introduce a four
atomic terms a through d. The proof Pf
must be explicitly allowed to rely on those terms, which is why
(Pf a b c d) is written instead of just Pf.
|check=decl}%
%define p1 = Pf
%solve _ : {a}{b}{c}{d} mklist (a + (b + c) + d) _ (Pf a b c d).
%{ == Tactics with %clause == }%
%{ Another way to achieve the same goal is to ''define'' (list A B)
as (pf (A == B)), which we do for list' below. }%
list' : num -> num -> type = [a][b] pf (a == b).
%mode list +A -B.
%{ Then, we have to ''justify'' each clause of the logic in the same way
as we justified plus_elim2 above. We have to write the
[[%clause]] because, if we do not, then Twelf will not use the
definitions in logic programming search. }%
%clause
list'-swap : list' (A + (B + C)) D
<- list' (A + B + C) D
= [Pf: pf (A + B + C == D)] (trans (symm plus_assoc) Pf).
%clause
list'-step : list' (A + C) (B + C)
<- list' A B
= [Pf: pf (A == B)] (plus_cong Pf refl).
%clause
list'-stop : list' A A = refl.
%{ Now we do not need to use %define; p2 proves the
same thing as p1 above. |check=decl}%
%solve p2 : {a}{b}{c}{d} list' (a + (b + c) + d) _.
%{ This is not the recommended style for a number of reasons (metatheoretic parts of Twelf like [[totality assertions]] won't work with %clause), but a few large examples such as the [[big algebraic solver]] have been written in this style. }%