Posted by Jesper on October 4, 2019
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.
open import Agda.Primitive open import Agda.Builtin.Bool open import Agda.Builtin.Nat open import Agda.Builtin.Equality
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.
variable A B C : Set x y z : A k l m n : Nat
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).
it : {{x : A}} → A
it {{x}} = x
(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.
_ : Nat _ = zero + 7 * (suc 3 - 1)
(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.
data Maybe (A : Set) : Set where just : A → Maybe A nothing : Maybe A mapMaybe : (A → B) → (Maybe A → Maybe B) mapMaybe f (just x) = just (f x) mapMaybe f nothing = nothing
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.
record _×_ (A B : Set) : Set where
constructor _,_
field
fst : A
snd : B
open _×_
A proof of ‘A or B’ is either inl x for a proof x : A or inr y
for a proof y : B.
data _⊎_ (A B : Set) : Set where inl : A → A ⊎ B inr : B → A ⊎ B mapInl : (A → B) → A ⊎ C → B ⊎ C mapInl f (inl x) = inl (f x) mapInl f (inr y) = inr y mapInr : (B → C) → A ⊎ B → A ⊎ C mapInr f (inl x) = inl x mapInr f (inr y) = inr (f y)
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 ⊤.
record ⊤ : Set where constructor tt -- no fields
‘false’ has no proofs.
data ⊥ : Set where -- no constructor
‘not A’ can be defined as ‘A implies false’.
¬_ : Set → Set ¬ A = A → ⊥
Examples
-- “If A then B implies A” ex₁ : A → (B → A) ex₁ = λ z _ → z -- “If A and true then A or false” ex₂ : (A × ⊤) → (A ⊎ ⊥) ex₂ = λ z → inl (fst z) -- “If A implies B and B implies C then A implies C” ex₃ : (A → B) → (B → C) → (A → C) ex₃ = λ f g z → g (f z) -- “It is not the case that not (either A or not A)” ex₄ : ¬ (¬ (A ⊎ (¬ A))) ex₄ = λ f → f (inr (λ x → f (inl x)))
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).
_ : x ≡ x _ = refl sym : x ≡ y → y ≡ x sym refl = refl trans : x ≡ y → y ≡ z → x ≡ z trans refl refl = refl cong : (f : A → B) → x ≡ y → f x ≡ f y cong f refl = refl subst : (P : A → Set) → x ≡ y → P x → P y subst P refl p = p
Ordering natural numbers
The standard ordering on natural numbers can be defined as an
indexed datatype
with two indices of type Nat:
module Nat-≤ where
data _≤_ : Nat → Nat → Set where
≤-zero : zero ≤ n
≤-suc : m ≤ n → suc m ≤ suc n
≤-refl : n ≤ n
≤-refl {n = zero} = ≤-zero
≤-refl {n = suc k} = ≤-suc ≤-refl
≤-trans : k ≤ l → l ≤ m → k ≤ m
≤-trans ≤-zero l≤m = ≤-zero
≤-trans (≤-suc k≤l) (≤-suc l≤m) =
≤-suc (≤-trans k≤l l≤m)
≤-antisym : m ≤ n → n ≤ m → m ≡ n
≤-antisym ≤-zero ≤-zero = refl
≤-antisym (≤-suc m≤n) (≤-suc n≤m) =
cong suc (≤-antisym m≤n n≤m)
Now we can prove statements like 3 ≤ 5 as follows:
_ : 3 ≤ 5 _ = ≤-suc (≤-suc (≤-suc ≤-zero))
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.
So : Bool → Set
So false = ⊥
So true = ⊤
instance
≤-dec : {p : So (m < suc n)} → m ≤ n
≤-dec {m = zero} {n = n} = ≤-zero
≤-dec {m = suc m} {n = suc n} {p = p} =
≤-suc (≤-dec {p = p})
_ : 9000 ≤ 9001
_ = it
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.
record Ord (A : Set) : Set₁ where
field
_≤_ : A → A → Set
≤-refl : x ≤ x
≤-trans : x ≤ y → y ≤ z → x ≤ z
≤-antisym : x ≤ y → y ≤ x → x ≡ y
_≥_ : A → A → Set
x ≥ y = y ≤ x
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.
open Ord {{...}}
We now define some concrete instances of the typeclass using copattern matching
instance
Ord-Nat : Ord Nat
_≤_ {{Ord-Nat}} = Nat-≤._≤_
≤-refl {{Ord-Nat}} = Nat-≤.≤-refl
≤-trans {{Ord-Nat}} = Nat-≤.≤-trans
≤-antisym {{Ord-Nat}} = Nat-≤.≤-antisym
instance
Ord-⊤ : Ord ⊤
_≤_ {{Ord-⊤}} = λ _ _ → ⊤
≤-refl {{Ord-⊤}} = tt
≤-trans {{Ord-⊤}} = λ _ _ → tt
≤-antisym {{Ord-⊤}} = λ _ _ → refl
module Example (A : Set) {{A-≤ : Ord A}} where
example : {x y z : A} → x ≤ y → y ≤ z → z ≤ x → x ≡ y
example x≤y y≤z z≤x = ≤-antisym {A = A} x≤y (≤-trans {A = A} y≤z z≤x)
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.
data Tri {{_ : Ord A}} : A → A → Set where
less : {{x≤y : x ≤ y}} → Tri x y
equal : {{x≡y : x ≡ y}} → Tri x y
greater : {{x≥y : x ≥ y}} → Tri x y
record TDO (A : Set) : Set₁ where
field
{{Ord-A}} : Ord A -- superclass Ord
tri : (x y : A) → Tri x y
open TDO {{...}} public
triNat : (x y : Nat) → Tri x y
triNat zero zero = equal
triNat zero (suc y) = less
triNat (suc x) zero = greater
triNat (suc x) (suc y) with triNat x y
... | less {{x≤y}} = less {{x≤y = Nat-≤.≤-suc x≤y}}
... | equal {{x≡y}} = equal {{x≡y = cong suc x≡y}}
... | greater {{x≥y}} = greater {{x≥y = Nat-≤.≤-suc x≥y}}
instance
TDO-Nat : TDO Nat
Ord-A {{TDO-Nat}} = Ord-Nat
tri {{TDO-Nat}} = triNat
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.
data [_]∞ (A : Set) : Set where
-∞ : [ A ]∞
[_] : A → [ A ]∞
+∞ : [ A ]∞
variable
lower upper : [ A ]∞
module Ord-[]∞ {A : Set} {{ A-≤ : Ord A}} where
data _≤∞_ : [ A ]∞ → [ A ]∞ → Set where
-∞-≤ : -∞ ≤∞ y
[]-≤ : x ≤ y → [ x ] ≤∞ [ y ]
+∞-≤ : x ≤∞ +∞
[]∞-refl : x ≤∞ x
[]∞-refl { -∞} = -∞-≤
[]∞-refl {[ x ]} = []-≤ (≤-refl {A = A})
[]∞-refl { +∞} = +∞-≤
[]∞-trans : x ≤∞ y → y ≤∞ z → x ≤∞ z
[]∞-trans -∞-≤ _ = -∞-≤
[]∞-trans ([]-≤ x≤y) ([]-≤ y≤z) = []-≤ (≤-trans {A = A} x≤y y≤z)
[]∞-trans _ +∞-≤ = +∞-≤
[]∞-antisym : x ≤∞ y → y ≤∞ x → x ≡ y
[]∞-antisym -∞-≤ -∞-≤ = refl
[]∞-antisym ([]-≤ x≤y) ([]-≤ y≤x) = cong [_] (≤-antisym x≤y y≤x)
[]∞-antisym +∞-≤ +∞-≤ = refl
instance
Ord-[]∞ : {{_ : Ord A}} → Ord [ A ]∞
_≤_ {{Ord-[]∞}} = _≤∞_
≤-refl {{Ord-[]∞}} = []∞-refl
≤-trans {{Ord-[]∞}} = []∞-trans
≤-antisym {{Ord-[]∞}} = []∞-antisym
open Ord-[]∞ public
We define some instances to automatically construct inequality proofs.
module _ {{_ : Ord A}} where
instance
-∞-≤-I : {y : [ A ]∞} → -∞ ≤ y
-∞-≤-I = -∞-≤
+∞-≤-I : {x : [ A ]∞} → x ≤ +∞
+∞-≤-I = +∞-≤
[]-≤-I : {x y : A} {{x≤y : x ≤ y}} → [ x ] ≤ [ y ]
[]-≤-I {{x≤y = x≤y}} = []-≤ x≤y
Now we are (finally) ready to define binary search trees.
data BST (A : Set) {{_ : Ord A}}
(lower upper : [ A ]∞) : Set where
leaf : {{l≤u : lower ≤ upper}}
→ BST A lower upper
node : (x : A)
→ BST A lower [ x ]
→ BST A [ x ] upper
→ BST A lower upper
_ : BST Nat -∞ +∞
_ = node 42
(node 6 leaf leaf)
(node 9000 leaf leaf)
Note how instances help by automatically filling in the proofs that the bounds are satisfied! Somewhat more explicitly, the tree looks as follows:
_ : BST Nat -∞ +∞
_ = node 42
(node 6 (leaf {{l≤u = -∞≤6}}) (leaf {{l≤u = 6≤42}}))
(node 9000 (leaf {{l≤u = 42≤9000}}) (leaf {{l≤u = 9000≤+∞}}))
where
-∞≤6 : -∞ ≤ [ 6 ]
-∞≤6 = it
6≤42 : [ 6 ] ≤ [ 42 ]
6≤42 = it
42≤9000 : [ 42 ] ≤ [ 9000 ]
42≤9000 = it
9000≤+∞ : [ 9000 ] ≤ +∞
9000≤+∞ = it
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.
module Lookup {{_ : TDO A}} where
data _∈_ {lower} {upper} (x : A) :
(t : BST A lower upper) → Set where
here : ∀ {t₁ t₂} → x ≡ y → x ∈ node y t₁ t₂
left : ∀ {t₁ t₂} → x ∈ t₁ → x ∈ node y t₁ t₂
right : ∀ {t₁ t₂} → x ∈ t₂ → x ∈ node y t₁ t₂
The definition of lookup makes use of
with-abstraction
to inspect the result of the tri function on x and y.
lookup : ∀ {lower} {upper}
→ (x : A) (t : BST A lower upper) → Maybe (x ∈ t)
lookup x leaf = nothing
lookup x (node y t₁ t₂) with tri x y
... | less = mapMaybe left (lookup x t₁)
... | equal = just (here it)
... | greater = mapMaybe right (lookup x t₂)
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).
module Insert {{_ : TDO A}} where
insert : (x : A) (t : BST A lower upper)
→ {{l≤x : lower ≤ [ x ]}} {{x≤u : [ x ] ≤ upper}}
→ BST A lower upper
insert x leaf = node x leaf leaf
insert x (node y t₁ t₂) with tri x y
... | less = node y (insert x t₁) t₂
... | equal = node y t₁ t₂
... | greater = node y t₁ (insert x t₂)
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!
open Lookup
insert-sound :
(x : A) (t : BST A lower upper)
→ {{_ : lower ≤ [ x ]}} {{_ : [ x ] ≤ upper}}
→ (x ≡ y) ⊎ (y ∈ t) → y ∈ insert x t
insert-sound x t (inl refl) = insert-sound₁ x t
where
insert-sound₁ :
(x : A) (t : BST A lower upper)
→ {{_ : lower ≤ [ x ]}} {{_ : [ x ] ≤ upper}}
→ x ∈ insert x t
insert-sound₁ x leaf = here refl
insert-sound₁ x (node y t₁ t₂) with tri x y
insert-sound₁ x (node y t₁ t₂) | less = left (insert-sound₁ x t₁)
insert-sound₁ x (node y t₁ t₂) | equal = here it
insert-sound₁ x (node y t₁ t₂) | greater = right (insert-sound₁ x t₂)
insert-sound x t (inr y∈t) = insert-sound₂ x t y∈t
where
insert-sound₂ :
(x : A) (t : BST A lower upper)
→ {{_ : lower ≤ [ x ]}} {{_ : [ x ] ≤ upper}}
→ y ∈ t → y ∈ insert x t
insert-sound₂ x (node y t₁ t₂) (here refl) with tri x y
... | less = here refl
... | equal = here refl
... | greater = here refl
insert-sound₂ x (node y t₁ t₂) (left y∈t₁) with tri x y
... | less = left (insert-sound₂ x t₁ y∈t₁)
... | equal = left y∈t₁
... | greater = left y∈t₁
insert-sound₂ x (node y t₁ t₂) (right y∈t₂) with tri x y
... | less = right y∈t₂
... | equal = right y∈t₂
... | greater = right (insert-sound₂ x t₂ y∈t₂)
insert-complete :
(x : A) (t : BST A lower upper)
→ {{_ : lower ≤ [ x ]}} {{_ : [ x ] ≤ upper}}
→ y ∈ insert x t → (x ≡ y) ⊎ (y ∈ t)
insert-complete x leaf (here refl) = inl refl
insert-complete x (node y t₁ t₂) y∈t' with tri x y
insert-complete x (node y t₁ t₂) (here refl) | less = inr (here refl)
insert-complete x (node y t₁ t₂) (here refl) | equal = inl it
insert-complete x (node y t₁ t₂) (here refl) | greater = inr (here refl)
insert-complete x (node y t₁ t₂) (left y∈t₁') | less = mapInr left (insert-complete x t₁ y∈t₁')
insert-complete x (node y t₁ t₂) (left y∈t₁) | equal = inr (left y∈t₁)
insert-complete x (node y t₁ t₂) (left y∈t₁) | greater = inr (left y∈t₁)
insert-complete x (node y t₁ t₂) (right y∈t₂) | less = inr (right y∈t₂)
insert-complete x (node y t₁ t₂) (right y∈t₂) | equal = inr (right y∈t₂)
insert-complete x (node y t₁ t₂) (right y∈t₂') | greater = mapInr right (insert-complete x t₂ y∈t₂')
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!