The Hydra is mainly a Rose tree, that is a finite tree with arbitrary branching. Hercules tries to kill the Hydra by chopping of heads (ie leaves of the Hydra) but the Hydra responds in growing copies of specific sub-trees determined by which head was chopped last.
The Hercules and Hydra game consists in Hercules chopping off heads of the Hydra until the Hydra has no more heads left. In response to a head cut, the Hydra may grow an arbitrary number of copies of the sub-tree rooted two levels below the head that was last chopped by Hercules.
Importantly the previous rule excludes root leaves (ie a leaf just above the root of the Hydra). If Hercules chops a root leaf, then the Hydra cannot morph in response: it must wait for the next move of Hercules. Absent of that exclusion, the game would possibly not end because nothing would force the number of nodes to (sometimes) decrease after the response of Hydra.
There are variants of the game where the number of copies that the Hydra makes is determined fully by the advancement of the game (eg the number of head that were chopped by Hercules so far) by this does not affect the termination property of the game.
This standalone (one file) proof was implemented by Dominique Larchey-Wendling,
see hydra.v
. It is distributed under the terms
of the MPL-2.0
Mozilla Public License.
In the code of hydra.v
, we only model a round
which is
the combination of a head cut by Hercules and a conforming response by the Hydra
(see above).
We do not model the moves of Hercules and Hydra independently because:
- while a move of Hercules is nondeterministic and only depends on the current shape of the Hydra;
- the response from the Hydra (also possibly nondeterministic), is still constrained by which head was chopped off last. The growing of copies of sub-hydras depends on the position of this last chopped head.
Modeling this dependency would lengthen the code base because we would need to represent the position of a head inside the Hydra so that Hercules can transmit that information. By combining Hercules move and the Hydra's response into the notion of round, the information can be transmitted without requiring external information.
As strange as it sounds, Hercules cannot avoid winning the game. To be fair, he will probably die out of exhaustion, or run out of energy, and if not, die of old age, before he actually chops of the last head of the Hydra. The game can take so long that possibly the universe itself would have contracted to another Big Bang before it ends.
However, in an ideal game, the Hydra is bound to be killed whatever strategy Hercules may use to chop of heads. The succession of rounds is strongly terminating and the only terminal point is a killed Hydra.
This is what we prove in the code and it takes less than 300 lines of Coq code to do it, not counting the small part of the standard library that we put to contribution.
Contrary to Kirby and Paris' proof implemented in Hydra Battles,
we do not use ordinals to show termination. Instead, we rely on the list path ordering lpo
, a weak variant
of the recursive path ordering pioneered by Dershowitz. By weak, we mean
minimized/tailored to the task given here.
The tiny well-foundedness proof of the lpo
displayed here however uses a
direct approach (as opposed to relying on Kruskal's tree theorem), inspired
by the work of Coupet-Grimal & Delobel (and also Goubault-Larrecq). The instance
we give here is just five lines of proof scripts containing 3 nested inductions.
It however relies on the accessibility characterization of the list ordering, of which the proof mimics the outline of Nipkow (and Buchholtz) for the well-foundedness of the multiset ordering. This proof is beautiful but a bit longer.
It is then a quite straightforward exercise to show that the round
relation is
included in the reverse of the lpo
, hence it is strongly terminating.
We only implement termination. We do not show that main result of Kirby and Paris contribution, that is the incapacity of Peano arithmetic to proof the strong termination of the game. This is a much longer endeavor that essentially requires showing that the length of a Hydra battle is function growing too fast to be represented in (first order) Peano arithmetic.
In this code, we implement hydras as an inductive type nested with lists:
Inductive hydra :=
| hydra_cons : list hydra → hydra.
whereas the Hydra Battles version favors a mutual inductive type instead:
Inductive Hydra :=
| Hydra_node : Hydrae → Hydra
with Hydrae :=
| Hydrae_nil : Hydrae
| Hydrae_cons : Hydra → Hydrae → Hydrae.
and we notice that the type Hydrae
is an isomorphic copy of the type list Hydra
.
IMHO there is only one reason to favor the mutual version Hydra
/Hydrae
over the nested
hydra
/list hydra
version: Coq can generate mutual inductive schemes for you and you
do not have to deal with mutual or nested fixpoints and intricacies the guard condition.
On the downside however, the type Hydrae
is not identical to list Hydra
and thus
one cannot use all the generic tools of the List
library. You basically have to
rewrite those of the List
tools you need. This is really a painful endeavor that unnecessarily
lengthen the code base. Should you want to define a variant of Hydra/Hydrae
, eg by
decorating nodes with data, and you get yet another copy of list _
which
requires another partial rewriting of the List
library.
On the other hand, working with the nested types hydra
/list hydra
requires you
to master nested fixpoints because Coq (so far) does not generate powerful enough
recursors (ie induction principles) for nested types. But they can be build by hand:
- first of all, this is a good incentive to learn working with guarded fixpoints directly and how Coq actually builds recursors;
- you can work with
list hydra
using the tools of theList
library as they are; - the code is now much more succinct and if you need to extend the
List
library, your extension can be used elsewhere. Same remark if you extendhydra
to decorated roses trees as eg:No need to duplicate everything.Inductive ltree X := | ltree_cons : X → list (ltree X) → ltree X.
So apart from the initial difficulty of having to understand how the guard condition works, which is btw something you should do anyway to become fluent in working with inductive types, there are only advantages with the choice of nesting over mutual.
To compare and illustrate the practical differences between the mutual and nested
versions, it is a very good exercise to implement the isomorphism between hydra
and Hydra
in Coq. You will learn how to deal with nested
fixpoints and mutual fixpoints.
This code contains, as an extra educational contribution, the construction of the
isomorphism between hydra
and Hydra
:
- we first represent the isomorphism as an inductive relation. This follows the general approach of the Braga method;
- we then show that this relation is functional and injective;
- we then realize that relation by fully specified functions in both directions;
- finally, we show that this gives a functional bijection, and derive convenient fixpoint equations for the isomorphism.