This page is meant to be used as a playground to experiment with Coq, directly in your web-browser.
It is the tradition in Coq to have a very crude language and packages that can be loaded at will by users to provide the primitives that will be used by users. For this example, we will load packages describing simple string operations, arithmetic with unbounded integers, lists and classical logical reasoning.
The advantage of higher-order logic is that we can reason about programs depending on fragments that have not been developed yet. To illustrate this, we propose an example of banking transactions relying on a database of accounts, where we suppose only the existence of two functions to access the value at a key in a given state of the database and to update the value at a key.
In the following Coq fragment, we open a section to give ourself a space where we make a few assumptions of existence. We assert the type of accounts and the two function operating on accounts exist. We also express properties about these functions, more precisely we describe how these functions interact together.
In this code fragment, line 4 declares that we open a new section.
Line 6 declares that we assume the existence of a type of data named
accounts
. Lines 8 and 9 assumes the existence of two functions
and gives their type. Line 8 says that the function get
takes
two arguments:
the first has type string
and the second one has
type accounts
, the return value has type Z
, a type
for unbounded integers. Line 9 says that function set
has
three arguments and returns a value of type accounts
.
string
Lines 11-12 assume that calling the function
get
on a database of accounts obtained by set
returns the
set value, if the same key is used for both set
and
get
.
Lines 14-15 assume that calling the function get
on a database
obtained by set
returns the same value as in the previous database, if the two keys used for set
and get
are different.
The forall sign (inverted A) and the arrow are obtained
by typing "forall" and "->". The "different" sign (a slashed equal sign), is obtained by typing <>
. Applying a function to several
arguments is written by typing the name of the function and then the arguments
separated by blank space. Parentheses are used only if one of the arguments is
itself obtained through a function call.
We also assume the existence of an initial database, where all possible accounts have value 0.
While in section assume_database_operations
, we can write
new code depending on get
and set
and we can
simulate its execution, by performing elementary proofs.
For instance, here is a piece of code that defines a new database of accounts with specifc values for a variety of keys
This code is incomplete, because we do not know how get
and set
are implemented. But we can execute this code
symbolically thanks to the behavior rules get_after_set_same_key
,
get_after_set_diff_key
, and init_zero
. The
symbolic execution will happen by performing proofs.
In Line 26, we assert that we want the execution of
get db5 "Denis"
to return the value 7. Line 27 has no effect other
than making our script more readable. Line 28 expands the definition of db5
. Line 29 is used to recognize that the current statement we
want to prove contains an instance of get_after_set_same_key
and
replaces that instance by the corresponding value. After executing that line,
the goal contains an obvious statement. We can solve this goal by using a very
simply proof command: easy
.
Time is too short to explain how automation works in the Coq system. For the
examples given on this page, we will need a proof finisher that is slightly
stronger than the command named easy
. Here is how we define it:
get
and set
Now we can prove something similar for the name Alice.
Note that line 34 generates two goals. The first goal is solved by lines 35-36. The second goal is kept for later and gets solved at line 37.
db5_alice
, you should be able to write
a lemma stating the expected value for Claude and Bernard.
We can now program a general operation to transfer money from one account to another.
Here is our solution
If we transfer an amount a from account x to account y, and in y's account before the transfer was n, what should be the value in y's account after the transfer? Write a statement that expresses it, and prove it.
The lesson of the last proof is that the naive expectation about the resulting amount for the receiver is not satisfied if the emitter and receiver are the same person. The proof process makes it clear: in this sense, it helps catching potential bugs by showing that easy general statements are sometimes not provable. By doing so, it forces users to document more precisely the conditions under which code behaves as expected.
When we close the section assume_database_operations
, we
obtain a higher-order transfer function, that will work on top of any database
implementation, as long as such a database satisfies the required properties.
The last Check
bcommand shows that the proof we did for
transfer_receiver
still holds, but it requires that we provide
the datatype for account databases, the two functions get
and
set
and the proofs that they have the right properties.
As a first illustration, we show that the datatype can be encoded using
lists of pairs, which record in order all the set operations that have
been done since the beginning. This has the advantage that the set
operation takes constant time, but the get
operation has a
complexity that grows with time (especially for accounts that are seldom
modified).
We can now show that the transfer function works in this setting.
The function l_transfer
is the function transfer
specialised on this naive implementation of account databases. Developing
a more efficient implementation is also possible in a few lines. Contact Yves Bertot at Inria for more information.