• No results found

Recursive Predicates

We have already seen all of the ingredients we need to build interesting recursive predicates, like this predicate capturing even-ness.

Inductive even : nat→ Prop :=

| EvenO : even O

| EvenSS :∀ n, even neven (S (Sn)).

Think of even as another judgment defined by natural deduction rules. The rule EvenO has nothing above the line and even O below the line, and EvenSS is a rule with even n

above the line and even (S (Sn)) below.

The proof techniques of the last section are easily adapted. Theoremeven 0 : even 0.

constructor. Qed.

Theoremeven 4 : even 4.

constructor; constructor; constructor. Qed.

It is not hard to see that sequences of constructor applications like the above can get tedious. We can avoid them using Coq’s hint facility, with a new Hint variant that asks

to consider all constructors of an inductive type during proof search. The tactic auto per- forms exhaustive proof search up to a fixed depth, considering only the proof steps we have registered as hints.

Hint Constructors even. Theoremeven 4’ : even 4.

auto. Qed.

We may also use inversion with even. Theoremeven 1 contra : even 1→ False.

inversion1. Qed.

Theoremeven 3 contra : even 3→ False. inversion1. H : even 3 n : nat H1 : even 1 H0 : n = 1 ============================ False

The inversion tactic can be a little overzealous at times, as we can see here with the introduction of the unused variable n and an equality hypothesis about it. For more complicated predicates, though, adding such assumptions is critical to dealing with the undecidability of general inversion. More complex inductive definitions and theorems can cause inversion to generate equalities where neither side is a variable.

inversionH1. Qed.

We can also do inductive proofs about even.

Theoremeven plus : ∀n m, even neven meven (n + m). It seems a reasonable first choice to proceed by induction on n. inductionn; crush.

n : nat

IHn : ∀m : nat, even neven meven (n + m)

m : nat

H : even (Sn)

H0 : even m

even (S (n + m))

We will need to use the hypotheses H and H0 somehow. The most natural choice is to invert H.

inversionH.

n : nat

IHn : ∀m : nat, even neven meven (n + m)

m : nat H : even (Sn) H0 : even m n0 : nat H2 : even n0 H1 : Sn0 = n ============================ even (S (Sn0 +m))

Simplifying the conclusion brings us to a point where we can apply a constructor. simpl. ============================ even (S (S(n0 + m))) constructor. ============================ even (n0 + m)

At this point, we would like to apply the inductive hypothesis, which is:

IHn : ∀m : nat, even neven meven (n + m)

Unfortunately, the goal mentions n0 where it would need to mention n to match IHn. We could keep looking for a way to finish this proof from here, but it turns out that we can make our lives much easier by changing our basic strategy. Instead of inducting on the structure ofn, we should inducton the structure of one of theevenproofs. This technique is commonly called rule induction in programming language semantics. In the setting of Coq, we have already seen how predicates are defined using the same inductive type mechanism as datatypes, so the fundamental unity of rule induction with “normal” induction is apparent.

Recall that tactics like induction and destruct may be passed numbers to refer to unnamed lefthand sides of implications in the conclusion, where the argument n refers to the nth such hypothesis.

Restart. induction1. m : nat ============================ even meven (0 + m) subgoal2 is: even meven (S (S n) + m)

The first case is easily discharged by crush, based on the hint we added earlier to try the constructors of even.

crush.

Now we focus on the second case: intro.

m : nat n : nat H : even n

IHeven : even meven (n +m)

H0 : even m

============================ even (S (Sn) + m)

We simplify and apply a constructor, as in our last proof attempt. simpl;constructor.

============================ even (n + m)

Now we have an exact match with our inductive hypothesis, and the remainder of the proof is trivial.

apply IHeven; assumption.

In fact, crush can handle all of the details of the proof once we declare the induction strategy.

Restart.

induction1; crush. Qed.

Induction on recursive predicates has similar pitfalls to those we encountered with inver- sion in the last section.

Theoremeven contra : ∀ n,even (S (n + n)) → False. induction1. n : nat ============================ False subgoal2 is: False

We are already sunk trying to prove the first subgoal, since the argument to even was replaced by a fresh variable internally. This time, we find it easier to prove this theorem by way of a lemma. Instead of trusting induction to replace expressions with fresh variables, we do it ourselves, explicitly adding the appropriate equalities as new assumptions.

Abort.

Lemma even contra’ : ∀ n’,even n’ → ∀ n, n’ = S(n + n)→ False. induction1; crush.

At this point, it is useful to consider all cases of n and n0 being zero or nonzero. Only one of these cases has any trickiness to it.

destructn; destructn0;crush.

n : nat

H : even (Sn)

IHeven : ∀ n0 : nat, S n = S(n0 + n0) →False n0 : nat

H0 : Sn =n0 + Sn0

============================

False

At this point it is useful to use a theorem from the standard library, which we also proved with a different name in the last chapter. We can search for a theorem that allows us to rewrite terms of the form x + S y.

SearchRewrite( + S ).

rewrite← plus n Sm in H0.

The induction hypothesis lets us complete the proof, if we use a variant of apply that has a with clause to give instantiations of quantified variables.

apply IHeven with n0; assumption.

As usual, we can rewrite the proof to avoid referencing any locally generated names, which makes our proof script more readable and more robust to changes in the theorem statement. We use the notation ← to request a hint that does right-to-left rewriting, just like we can with therewrite tactic.

Restart.

Hint Rewrite← plus n Sm. induction1; crush;

match goal with

| [H : S ?N = ?N0 + ?N0 ` ] ⇒ destruct N; destructN0

end; crush. Qed.

We write the proof in a way that avoids the use of local variable or hypothesis names, using thematchtactic form to do pattern-matching on the goal. We use unification variables prefixed by question marks in the pattern, and we take advantage of the possibility to mention a unification variable twice in one pattern, to enforce equality between occurrences. The hint to rewrite with plus n Sm in a particular direction saves us from having to figure out the right place to apply that theorem.

The original theorem now follows trivially from our lemma, using a new tactic eauto, a fancier version of auto whose explanation we postpone to Chapter 13.

Theoremeven contra : ∀ n,even (S (n + n)) → False. intros; eapply even contra’;eauto.

Qed.

We use a varianteapplyof applywhich has the same relationship toapplyaseautohas toauto. An invocation of applyonly succeeds if all arguments to the rule being used can be determined from the form of the goal, whereaseapplywill introduce unification variables for undetermined arguments. In this case, eauto is able to determine the right values for those unification variables, using (unsurprisingly) a variant of the classic algorithm forunification

[41].

By considering an alternate attempt at proving the lemma, we can see another common pitfall of inductive proofs in Coq. Imagine that we had tried to prove even contra’ with all of the ∀ quantifiers moved to the front of the lemma statement.

Lemma even contra’’ : ∀ n’ n, even n’n’ = S(n + n)→ False. induction1; crush;

match goal with

| [H : S ?N = ?N0 + ?N0 ` ] ⇒ destruct N; destructN0

One subgoal remains: n : nat H : even (S(n +n)) IHeven : S(n +n) = S (S(S (n + n))) → False ============================ False

We are out of luck here. The inductive hypothesis is trivially true, since its assumption is false. In the version of this proof that succeeded, IHeven had an explicit quantification over n. This is because the quantification of n appeared after the thing we are inducting on in the theorem statement. In general, quantified variables and hypotheses that appear before the induction object in the theorem statement stay fixed throughout the inductive proof. Variables and hypotheses that are quantified after the induction object may be varied explicitly in uses of inductive hypotheses.

Abort.

Why should Coq implementinductionthis way? One answer is that it avoids burdening this basic tactic with additional heuristic smarts, but that is not the whole picture. Imagine that induction analyzed dependencies among variables and reordered quantifiers to pre- serve as much freedom as possible in later uses of inductive hypotheses. This could make the inductive hypotheses more complex, which could in turn cause particular automation machinery to fail when it would have succeeded before. In general, we want to avoid quan- tifiers in our proofs whenever we can, and that goal is furthered by the refactoring that the induction tactic forces us to do.

Chapter 5

Infinite Data and Proofs

In lazy functional programming languages like Haskell, infinite data structures are every- where [16]. Infinite lists and more exotic datatypes provide convenient abstractions for communication between parts of a program. Achieving similar convenience without infinite lazy structures would, in many cases, require acrobatic inversions of control flow.

Laziness is easy to implement in Haskell, where all the definitions in a program may be thought of as mutually recursive. In such an unconstrained setting, it is easy to implement an infinite loop when you really meant to build an infinite list, where any finite prefix of the list should be forceable in finite time. Haskell programmers learn how to avoid such slip-ups. In Coq, such a laissez-faire policy is not good enough.

We spent some time in the last chapter discussing the Curry-Howard isomorphism, where proofs are identified with functional programs. In such a setting, infinite loops, intended or otherwise, are disastrous. If Coq allowed the full breadth of definitions that Haskell did, we could code up an infinite loop and use it to prove any proposition vacuously. That is, the addition of general recursion would make CICinconsistent. For an arbitrary proposition P, we could write:

Fixpoint bad (u : unit) : P := bad u.

This would leave us with bad ttas a proof of P.

There are also algorithmic considerations that make universal termination very desirable. We have seen how tactics like reflexivitycompare terms up to equivalence under compu- tational rules. Calls to recursive, pattern-matching functions are simplified automatically, with no need for explicit proof steps. It would be very hard to hold onto that kind of benefit if it became possible to write non-terminating programs; we would be running smack into the halting problem.

One solution is to use types to contain the possibility of non-termination. For instance, we can create a “non-termination monad,” inside which we must write all of our general- recursive programs; several such approaches are surveyed in Chapter 7. This is a heavyweight solution, and so we would like to avoid it whenever possible.

Luckily, Coq has special support for a class of lazy data structures that happens to contain most examples found in Haskell. That mechanism, co-inductive types, is the subject

of this chapter.

5.1

Computing with Infinite Data

Let us begin with the most basic type of infinite data,streams, or lazy lists. Sectionstream.

VariableA : Type.

CoInductivestream: Type :=

|Cons : Astreamstream. End stream.

The definition is surprisingly simple. Starting from the definition of list, we just need to change the keyword InductivetoCoInductive. We could have left a Nilconstructor in our definition, but we will leave it out to force all of our streams to be infinite.

How do we write down a stream constant? Obviously, simple application of constructors is not good enough, since we could only denote finite objects that way. Rather, whereas recursive definitions were necessary tousevalues of recursive inductive types effectively, here we find that we needco-recursive definitions tobuild values of co-inductive types effectively.

We can define a stream consisting only of zeroes. CoFixpoint zeroes : streamnat :=Cons 0 zeroes.

We can also define a stream that alternates between true and false. CoFixpoint trues falses : streambool := Cons true falses trues

with falses trues : stream bool := Consfalse trues falses.

Co-inductive values are fair game as arguments to recursive functions, and we can use that fact to write a function to take a finite approximation of a stream.

Fixpoint approx A (s : stream A) (n : nat) : list A:= match n with | O ⇒ nil | S n’ ⇒ match s with | Cons h th :: approx t n’ end end.

Eval simpl in approx zeroes 10.

= 0 :: 0 :: 0 :: 0 :: 0 :: 0 :: 0 :: 0 :: 0 :: 0 :: nil

: list nat

Eval simpl in approx trues falses 10. =true

:: false

:: true

:: false

:: true :: false :: true :: false :: true :: false :: nil

: list bool

So far, it looks like co-inductive types might be a magic bullet, allowing us to import all of the Haskeller’s usual tricks. However, there are important restrictions that are dual to the restrictions on the use of inductive types. Fixpoints consume values of inductive types, with restrictions on which arguments may be passed in recursive calls. Dually, co-fixpoints

produce values of co-inductive types, with restrictions on what may be done with theresults

of co-recursive calls.

The restriction for co-inductive types shows up as the guardedness condition. First, consider this stream definition, which would be legal in Haskell.

CoFixpoint looper : stream nat:= looper. Error:

Recursive definition of looper is ill-formed. In environment

looper : stream nat

unguarded recursive call in "looper"

The rule we have run afoul of here is that every co-recursive call must be guarded by a constructor; that is, every co-recursive call must be a direct argument to a constructor of the co-inductive type we are generating. It is a good thing that this rule is enforced. If the definition oflooperwere accepted, ourapprox function would run forever when passedlooper, and we would have fallen into inconsistency.

Some familiar functions are easy to write in co-recursive fashion. Sectionmap.

VariablesA B :Type. Variablef : AB.

CoFixpoint map (s : stream A) :streamB := match s with

| Consh t ⇒ Cons(f h) (map t) end.

End map.

This code is a literal copy of that for the listmap function, with thenilcase removed and Fixpoint changed to CoFixpoint. Many other standard functions on lazy data structures can be implemented just as easily. Some, like filter, cannot be implemented. Since the predicate passed to filter may reject every element of the stream, we cannot satisfy the guardedness condition.

The implications of the condition can be subtle. To illustrate how, we start off with another co-recursive function definition that is legal. The function interleave takes two streams and produces a new stream that alternates between their elements.

Sectioninterleave. VariableA : Type.

CoFixpoint interleave (s1 s2 : stream A) :stream A := match s1, s2 with

| Consh1 t1, Cons h2 t2 ⇒ Consh1 (Cons h2 (interleave t1 t2)) end.

End interleave.

Now say we want to write a weird stuttering version of map that repeats elements in a particular way, based on interleaving.

Sectionmap’.

VariablesA B :Type. Variablef : AB.

CoFixpoint map’ (s : streamA) : streamB := match s with

| Consh t ⇒ interleave (Cons(f h) (map’ t)) (Cons (f h) (map’ t)) end.

We get another error message about an unguarded recursive call. End map’.

What is going wrong here? Imagine that, instead of interleave, we had called some other, less well-behaved function on streams. Here is one simpler example demonstrating the essential pitfall. We start by defining a standard function for taking the tail of a stream. Since streams are infinite, this operation is total.

Definition tl A (s : streamA) :stream A := match s with

| Cons s’s’

end.

Coq rejects the following definition that uses tl. CoFixpoint bad : stream nat:= tl (Cons0 bad).

Imagine that Coq had accepted our definition, and consider how we might evaluateapprox bad 1. We would be trying to calculate the first element in the stream bad. However, it is not hard to see that the definition of bad “begs the question”: unfolding the definition of tl, we see that we essentially say “define bad to equal itself”! Of course such an equation admits no single well-defined solution, which does not fit well with the determinism of Gallina reduction.

Coq’s complete rule for co-recursive definitions includes not just the basic guardedness condition, but also a requirement about where co-recursive calls may occur. In particular,

a co-recursive call must be a direct argument to a constructor, nested only inside of other constructor calls orfun ormatchexpressions. In the definition ofbad, we erroneously nested the co-recursive call inside a call totl, and we nested inside a call tointerleavein the definition of map’.

Coq helps the user out a little by performing the guardedness check after using compu- tation to simplify terms. For instance, any co-recursive function definition can be expanded by inserting extra calls to an identity function, and this change preserves guardedness. How- ever, in other cases computational simplification can reveal why definitions are dangerous. Consider what happens when we inline the definition of tl in bad:

CoFixpoint bad : stream nat:= bad.

This is the same looping definition we rejected earlier. A similar inlining process reveals an alternate view on our failed definition of map’:

CoFixpoint map’ (s : stream A) :stream B := match s with

| Cons h t ⇒ Cons (f h) (Cons (f h) (interleave (map’ t) (map’ t))) end.

Clearly in this case the map’ calls are not immediate arguments to constructors, so we violate the guardedness condition.

A more interesting question is why that condition is the right one. We can make an intuitive argument that the original map’ definition is perfectly reasonable and denotes a well-understood transformation on streams, such that every output would behave properly with approx. The guardedness condition is an example of a syntactic check for productivity

of co-recursive definitions. A productive definition can be thought of as one whose outputs can be forced in finite time to any finite approximation level, as withapprox. If we replaced the guardedness condition with more involved checks, we might be able to detect and allow a broader range of productive definitions. However, mistakes in these checks could cause