Copatterns

Note

If you are looking for information on how to use copatterns with coinductive records, please visit the section on coinduction.

Consider the following record:

  1. record Enumeration (A : Set) : Set where
  2. constructor enumeration
  3. field
  4. start : A
  5. forward : A A
  6. backward : A A

This gives an interface that allows us to move along the elements of a data type A.

For example, we can get the “third” element of a type A:

  1. open Enumeration
  2. 3rd : {A : Set} Enumeration A A
  3. 3rd e = forward e (forward e (forward e (start e)))

Or we can go back 2 positions starting from a given a:

  1. backward-2 : {A : Set} Enumeration A A A
  2. backward-2 e a = backward (backward a)
  3. where
  4. open Enumeration e

Now, we want to use these methods on natural numbers. For this, we need a record of type Enumeration Nat. Without copatterns, we would specify all the fields in a single expression:

  1. open Enumeration
  2. enum-Nat : Enumeration Nat
  3. enum-Nat = record
  4. { start = 0
  5. ; forward = suc
  6. ; backward = pred
  7. }
  8. where
  9. pred : Nat Nat
  10. pred zero = zero
  11. pred (suc x) = x
  12. test : 3rd enum-Nat 3
  13. test = refl
  14. test : backward-2 enum-Nat 5 3
  15. test = refl

Note that if we want to use automated case-splitting and pattern matching to implement one of the fields, we need to do so in a separate definition.

With copatterns, we can define the fields of a record as separate declarations, in the same way that we would give different cases for a function:

  1. open Enumeration
  2. enum-Nat : Enumeration Nat
  3. start enum-Nat = 0
  4. forward enum-Nat n = suc n
  5. backward enum-Nat zero = zero
  6. backward enum-Nat (suc n) = n

The resulting behaviour is the same in both cases:

  1. test : 3rd enum-Nat 3
  2. test = refl
  3. test : backward-2 enum-Nat 5 3
  4. test = refl

Copatterns in function definitions

In fact, we do not need to start at 0. We can allow the user to specify the starting element.

Without copatterns, we just add the extra argument to the function declaration:

  1. open Enumeration
  2. enum-Nat : Nat Enumeration Nat
  3. enum-Nat initial = record
  4. { start = initial
  5. ; forward = suc
  6. ; backward = pred
  7. }
  8. where
  9. pred : Nat Nat
  10. pred zero = zero
  11. pred (suc x) = x
  12. test : 3rd (enum-Nat 10) 13
  13. test = refl

With copatterns, the function argument must be repeated once for each field in the record:

  1. open Enumeration
  2. enum-Nat : Nat Enumeration Nat
  3. start (enum-Nat initial) = initial
  4. forward (enum-Nat _) n = suc n
  5. backward (enum-Nat _) zero = zero
  6. backward (enum-Nat _) (suc n) = n

Mixing patterns and co-patterns

Instead of allowing an arbitrary value, we want to limit the user to two choices: 0 or 42.

Without copatterns, we would need an auxiliary definition to choose which value to start with based on the user-provided flag:

  1. open Enumeration
  2. if_then_else_ : {A : Set} Bool A A A
  3. if true then x else _ = x
  4. if false then _ else y = y
  5. enum-Nat : Bool Enumeration Nat
  6. enum-Nat ahead = record
  7. { start = if ahead then 42 else 0
  8. ; forward = suc
  9. ; backward = pred
  10. }
  11. where
  12. pred : Nat Nat
  13. pred zero = zero
  14. pred (suc x) = x

With copatterns, we can do the case analysis directly by pattern matching:

  1. open Enumeration
  2. enum-Nat : Bool Enumeration Nat
  3. start (enum-Nat true) = 42
  4. start (enum-Nat false) = 0
  5. forward (enum-Nat _) n = suc n
  6. backward (enum-Nat _) zero = zero
  7. backward (enum-Nat _) (suc n) = n

Tip

When using copatterns to define an element of a record type, the fields of the record must be in scope. In the examples above, we use open Enumeration to bring the fields of the record into scope.

Consider the first example:

  1. enum-Nat : Enumeration Nat
  2. start enum-Nat = 0
  3. forward enum-Nat n = suc n
  4. backward enum-Nat zero = zero
  5. backward enum-Nat (suc n) = n

If the fields of the Enumeration record are not in scope (in particular, the start field), then Agda will not be able to figure out what the first copattern means:

  1. Could not parse the left-hand side start enum-Nat
  2. Operators used in the grammar:
  3. None
  4. when scope checking the left-hand side start enum-Nat in the
  5. definition of enum-Nat

The solution is to open the record before using its fields:

  1. open Enumeration
  2. enum-Nat : Enumeration Nat
  3. start enum-Nat = 0
  4. forward enum-Nat n = suc n
  5. backward enum-Nat zero = zero
  6. backward enum-Nat (suc n) = n