I quite often hear from people that they are interested in learning Agda, and that’s great! However, I often feel there are not enough examples out there of how to use the different features of Agda to implement programs, enforce invariants in their types, and prove their properties. So in this blog post I hope to address exactly that problem.

The main goal of this example is to show off the dual purpose of Agda as a strongly typed programming language and as a proof assistant for formalizing mathematics. Since both purposes are part of the same language, each of them reinforces the other: we can prove properties of our programs, and we can write programs that produce proofs. In this example, we will see this power in action by (1) defining the mathematical structure of partial orders, (2) implementing a generic binary search trees and insertion of new elements into them, and (3) proving that this function is implemented correctly.

This blog post was based on a talk I gave at the Initial Types Club at Chalmers.

Preliminaries

For this post we keep the dependencies to a minimum so we don’t rely on the standard library. Instead, we import some of the built-in modules of Agda directly.

A variable declaration (since Agda 2.6.0) allows us to use variables without binding them explicitly. This means they are implicitly universally quantified in the types where they occur.

In the code that follows, we will use instance arguments to automatically construct some proofs. When working with instance arguments, the it function below is often very useful. All it does is ask Agda to please fill in the current argument by using a definition that is marked as an instance . (More about instance arguments later).

(Unary) natural numbers are defined as the datatype Nat with two constructors zero : Nat and suc : Nat → Nat . We use the ones imported from Agda.Builtin.Nat because they allow us to write literal numerals as well as constructor forms.

(Definitions that are named _ are typechecked by Agda but cannot be used later on. This is often used to define examples or test cases).

We can define parametrized datatypes and functions by pattern matching on them. For example, here is the equivalent of Haskell’s Maybe type.

Note how A and B are implicitly quantified in the type of mapMaybe !

Quick recap on the Curry-Howard correspondence

The Curry-Howard correspondence is the core idea that allows us to use Agda as both a programming language and a proof assistant. Under the Curry-Howard correspondence, we can interpret logical propositions (A ∧ B, ¬A, A ⇒ B, …) as the types of all their possible proofs.

A proof of ‘A and B’ is a pair (x , y) of a proof x : A and an proof y : B .

A proof of ‘A or B’ is either inl x for a proof x : A or inr y for a proof y : B .

A proof of ‘A implies B’ is a transformation from proofs x : A to proofs of B , i.e. a function of type A → B .

‘true’ has exactly one proof tt : ⊤ . We could define this as a datatype with a single constructor tt , but here we define it as a record type instead. This has the advantage that Agda will use eta-equality for elements of ⊤ , i.e. x = y for any two variables x and y of type ⊤ .

‘false’ has no proofs.

‘not A’ can be defined as ‘A implies false’.

Examples

Since Agda’s logic is constructive, it is not possible to prove the direct version of ex₄ ( A ⊎ (¬ A) ).

Equality

To state many properties of our programs, we need the notion of equality. In Agda, equality is defined as the datatype _≡_ with one constructor refl : x ≡ x (imported from Agda.Builtin.Equality ).

Ordering natural numbers

The standard ordering on natural numbers can be defined as an indexed datatype with two indices of type Nat :

Now we can prove statements like 3 ≤ 5 as follows:

However, to prove an inequality like 9000 ≤ 9001 we would have to write 9000 ≤-suc constructors, which would get very tedious. Instead, we can use Agda’s instance arguments to automatically construct these kind of proofs.

To do this, we define an ‘instance’ that automatically constructs a proof of m ≤ n when m and n are natural number literals. A definition inst : A that is marked as an ‘instance’ will be used to automatically construct the implicit argument to functions with a type of the form {{x : A}} → B .

For efficiency reasons, we don’t mark the constructors ≤-zero and ≤-suc as instances directly. Instead, we make use of the efficient boolean comparison _<_ (imported from Agda.Builtin.Nat ) to construct the instance when the precondition So (m < suc n) is satisfied.

Partial orders

We’d like to talk not just about orderings on concrete types like Nat , but also about the general concept of a ‘partial order’. For this purpose, we define a typeclass Ord that contains the type _≤_ and proofs of its properties.

Unlike in Haskell, typeclasses are not a primitive concept in Agda. Instead, we use the special syntax open Ord {{...}} to bring the fields of the record in scope as instance functions with a type of the form {A : Set}{{r : Ord A}} → ... . Instance search will then kick in to find the right implementation of the typeclass automatically.

We now define some concrete instances of the typeclass using copattern matching

For working with binary search trees, we need to be able to decide for any two elements which is the bigger one, i.e. we need a total, decidable order.

Binary search trees

In a dependently typed language, we can encode invariants of our data structures by using indexed datatypes. In this example, we will implement binary search trees by a lower and upper bound to the elements they contain (see How to Keep Your Neighbours in Order by Conor McBride).

Since the lower bound may be -∞ and the upper bound may be +∞, we start by providing a generic way to extend a partially ordered set with those two elements.

We define some instances to automatically construct inequality proofs.

Now we are (finally) ready to define binary search trees.

Note how instances help by automatically filling in the proofs that the bounds are satisfied! Somewhat more explicitly, the tree looks as follows:

Next up: defining a lookup function. The result of this function is not just a boolean true/false, but a proof that the element is indeed in the tree. A proof that x is in the tree t is either a proof that it is here , a proof that it is in the left subtree, or a proof that it is in the right subtree.

The definition of lookup makes use of with -abstraction to inspect the result of the tri function on x and y .

Similarly, we can define an insertion function. Here, we need to enforce the precondition that the element we want to insert is between the bounds (alternatively, we could have updated the bounds in the return type to ensure they include the inserted element).

To prove correctness of insertion, we have to show that y ∈ insert x t is equivalent to x ≡ y ⊎ y ∈ t . The proofs insert-sound₂ and insert-complete are a bit long because there are two elements x and y that can both independently be here , in the left subtree, or in the right subtree, so we have to distinguish 9 distinct cases. Let me know if you manage to find a shorter proof!

Of course, there are many more functions on search trees we could want to implement and prove correct: deletion, merging, flattening … Likewise, there are other invariants we might want to enforce in the type, such as being well-balanced. I strongly recommend to read Conor McBride’s paper on the topic, or try it yourself!