Theory Isabelle
theory Isabelle
imports
HOL.HOL
HOL.Int
HOL.Nat
begin
chapter ‹Introduction to Isabelle›
text ‹\label{chap:isabelle}›
text ‹Isabelle is an interactive proof assistant and Isabelle/HOL is an implementation of \emph{higher-order logic} in Isabelle. With it, one can interactively prove propositions about theories that are formalised in terms of higher-order \linebreak logic. Many theories have been formalised (and many theorems proven) in Isabelle/HOL and are publicly available.\footnote{see Isabelle's Archive of Formal Proofs at \code{\href{https://www.isa-afp.org}{isa-afp.org}}}
In this appendix, I will give a short introduction into the most important concepts of Isabelle. For an extensive tutorial, see @{cite prog_prove}. A complete documentation can be found in @{cite isar_ref}.
\vspace{-.3cm}›
subsubsection ‹Simple Definitions›
text ‹The command ‹definition› defines a term by establishing an equality, denoted by ‹≡›. This term can be a function or a constant (i.e.\@ 0-ary function). Predicates are functions to Boolean values.
Definitions are annotated by their type. As an example, we define the predicate ‹even›, which maps an integer to a Boolean value.›
definition even :: ‹int ⇒ bool›
where ‹even n ≡ ∃ m::int . n = 2 * m›
text ‹Functions can be defined in uncurried form (e.g.\@ ‹(int × int) ⇒ bool›) or in curried form (e.g.\@ ‹int ⇒ int ⇒ bool›). As a very trivial example, we can define equality predicates for integers. Compared to the curried version, the uncurried version does not allow for easy pattern matching. This is why, in this thesis, I usually specify functions in curried form.›
definition equal_uncurried :: ‹(int × int) ⇒ bool›
where ‹equal_uncurried pair ≡ ∃ n m. pair = (n, m) ∧ n = m›
definition equal_curried :: ‹int ⇒ int ⇒ bool›
where ‹equal_curried n m ≡ n = m›
text ‹We can also use type variables (prefixed with an apostrophe, e.g.\@ ‹'a›) instead of concrete types to get more abstract terms.›
definition equal_abstract :: ‹'a ⇒ 'a ⇒ bool›
where ‹equal_abstract a b ≡ a = b›
text ‹For a less trivial example, we define a predicate ‹symmetric› that determines whether a given relation is symmetric. An arbitrary homogeneous relation in curried form has the type ‹'a ⇒ 'a ⇒ bool›.›
definition symmetric :: ‹('a ⇒ 'a ⇒ bool) ⇒ bool›
where ‹symmetric R ≡ ∀ a b. R a b ⟶ R b a›
text ‹We can also assign notation to a term during the definition, where ‹_› is a placeholder (and the numbers behind the notation specification represent priorities for parsing, which may be ignored by the reader).›
definition approx :: ‹int ⇒ int ⇒ bool›
(‹_ ≈ _› [50, 50] 50)
where ‹n ≈ m ≡ n=m-1 ∨ n=m ∨ n=m+1›
text ‹Abbreviations are used the same way as definitions, except that, in order to use the equality established by definitions in proofs, we need to explicitly refer to the definition, whereas abbreviations are always expanded internally by the proof system. An example a little further down below should clarify the distinction.›
abbreviation reflexive :: ‹('a ⇒ 'a ⇒ bool) ⇒ bool›
where ‹reflexive R ≡ ∀ a. R a a›
subsubsection ‹Proving Propositions›
text ‹Propositions can be stated using any of the commands ‹proposition›, ‹lemma›, ‹theorem›, ‹corollary›, and require a proof.
Since Isabelle is an \emph{interactive} proof assistant, proofs are usually meant to be spelled out in code so as to be readable by humans, and the validity of individual steps is verified by certain automated proof methods (e.g.\@ ‹simp›, ‹arith›, ‹auto›, ‹fast›, ‹blast›, \dots).
As an example, we will show that the relation ‹approx› is ‹symmetric›.
Since ‹symmetric› was defined using the command ‹definition›, we need to explicitly unfold it, where ‹symmetric_def› is the fact (about the equality) introduced by the definition.
The method specified after the command ‹proof› adjusts the proof goal in some way. Ideally, the proof steps should be clear to the reader even without seeing what exactly the automated methods are doing. I have explained each of the steps using comments below.
\pagebreak›
proposition ‹symmetric approx›
unfolding symmetric_def
proof (clarify)
fix n m
assume ‹n ≈ m›
hence ‹n=m-1 ∨ n=m ∨ n=m+1› unfolding approx_def .
thus ‹m ≈ n›
proof (elim disjE)
assume ‹n = m - 1›
hence ‹m = n + 1› by arith
thus ‹m ≈ n› unfolding approx_def by blast
next
assume ‹n = m›
thus ‹m ≈ n› using approx_def by blast
next
assume ‹n = m + 1›
hence ‹m = n - 1› by arith
thus ‹m ≈ n› using approx_def by blast
qed
qed
text ‹This proof was probably more detailed than was necessary. By unfolding the other definition as well, this proposition can be proven directly with the proof method ‹arith›.›
proposition ‹symmetric approx›
unfolding symmetric_def approx_def by arith
text ‹To see the difference between definitions and abbreviations, note that the following proposition is provable without unfolding ‹reflexive_def› (since ‹reflexive› is an abbreviation, there is no such fact in this context).›
proposition ‹reflexive approx›
unfolding approx_def by auto
text ‹In practice, of course, one has to strike a balance between transparency/comprehensibility and conciseness of proofs.›
subsubsection ‹Inductive Definitions›
text ‹Inductively defined predicates can be given using premise-conclusion pairs and multiple clauses.›
inductive even_ind :: ‹int ⇒ bool›
where
‹even_ind 0›
| ‹even_ind n ⟹ even_ind (n+2)›
subsubsection ‹Function Definitions›
text ‹The command ‹function› also establishes equalities, but usually in more complex ways, so that it may not obvious whether a function is well-defined. Hence, the well-definedness needs to be proved explicitly. (These proofs are often solved by the automated proof methods.)
The function is then assumed to be partial. The command ‹termination› introduces proof obligations to show that the function always terminates (and is thus total). For the example below, this is again proved automatically.
After proving well-definedness and totality, we have access to facts about the function that can be used in proofs, e.g.\@ induction principles. More details can be found in \cref{sec:HML}, where we define a non-trivial ‹function›.›
function factorial :: ‹nat ⇒ nat›
where
‹n = 0 ⟹ factorial n = 1›
| ‹n > 0 ⟹ factorial n = n * factorial (n-1)›
by auto
termination factorial using "termination" by force
subsubsection ‹Data Types›
text ‹With the command ‹datatype›, new types can be defined, possibly in dependence on existing types, by defining a set of (object) constructor functions. For example, we can (re-)define the type of natural numbers.›
datatype natural_number =
Zero
| Suc ‹natural_number›
text ‹We can define type constructors, i.e.\@ types depending on other types (to be distinguished from the object constructors above) by parameterising the type with type variables.›
datatype ('a)list =
Empty
| Cons ‹'a› ‹('a)list›
subsubsection ‹Locales›
text ‹Locales define a context consisting of type variables, object variables, and assumptions. These can be accessed in the entire context. Locales can be instantiated by specifying concrete types/objects (or variables from another context) for the type/object variables, and extended to form new locales. We can reenter the locale contexts later on, using the command ‹context›. \Cref{sec:LTS} provides a good example for how locales are used in Isabelle to formalise linear transition systems.›
end