This page: bit.ly/coqosxp2022

A Coq tutorial about accounts and financial transactions

This page is meant to be used as a playground to experiment with Coq, directly in your web-browser.

A Coq header

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.

A little example about bank account management

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.

Explaining the get and set functions and their properties

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. stringLines 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.

An initial database

We also assume the existence of an initial database, where all possible accounts have value 0.

Reasoning abstractly on incomplete code

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.

More automation

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:

A proof that uses both properties of 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.

Your turn: solve an exercise

By reproducing the steps of db5_alice, you should be able to write a lemma stating the expected value for Claude and Bernard.

More advance programming: a transfer operation

We can now program a general operation to transfer money from one account to another.

Here is our solution

Expected values of accounts after a transfer

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.

Our solution to the last exercise

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.

Making the code more concrete

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 Checkbcommand 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.

.

A naive implementation based on lists of pairs

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.