1
votes

In Agda, it seems that often there are two ways to refine a set. One is by simply writing a function that checks if a property holds, and lift. For example:

has_true : List Bool -> Bool 
has_true (true ∷ xs) = true
has_true (false ∷ xs) = has_true xs
has_true [] = false

Truthy : List Bool -> Set
Truthy list = T (has_true list)

Here, Truthy list is a proof that a boolean list has at least one true element. The other way is by encoding that property directly, as an inductive type:

data Truthy : List Bool -> Set where
  Here  : (x : Bool) -> (x ≡ true) -> (xs : List Bool) -> Truthy (x ∷ xs)
  There : (x : Bool) -> (xs : List Bool) -> Truthy xs -> Truthy (x ∷ xs)

Here, Truthy list also proves the same thing.

I believe I have read a comparison before, but I don't remember. Is there a name for those different styles? What are the advantages and drawbacks of using one style over another? And is there a third option?

1
There is a comparison in Brutal Meta Introduction to Dependent Types in Agda under the "The magic of dependent types" and "Type families and Unification" headers.user3237465

1 Answers

7
votes

So far you listed two ways of defining predicates:

  1. A -> Bool functions.
  2. Inductive predicates.

I would add one more:

  1. A -> Set functions. Could be also called "recursively defined", or "defined by large elimination".

The third version is the following in Agda:

open import Data.Bool
open import Data.Unit
open import Data.Empty
open import Data.List

hastrue : List Bool → Set
hastrue []           = ⊥   -- empty type
hastrue (false ∷ bs) = hastrue bs
hastrue (true ∷ bs)  = ⊤   -- unit type

First, let's talk about what kind of predicates are representable using the three options. Here's an ASCII table. * is a wildcard standing for yes/no.

                    | P : A -> Set | P : A -> Bool | data P : A -> Set |
|-------------------|--------------|---------------|-------------------|
| Proof irrelevant  | *            | yes           | *                 |
| Structural        | yes          | yes           | *                 |
| Strictly positive | *            | N/A           | yes               |
| Decidable         | *            | yes           | *                 |

Proof irrelevance means that all proofs for P x are equal. In the Bool case, a proof is usually some p : P x ≡ true, or p : IsTrue (P x) with IsTrue = λ b → if b then ⊤ else ⊥, and in both cases all proofs are equal indeed. We may or may not want predicates to be irrelevant.

Structural means that P x can only be defined using elements of A which are structurally smaller than x. Functions are always structural, so if some predicate isn't, then it can only be defined inductively.

Strictly positive means that P cannot occur recursively to the left of a function arrow. Non-strictly positive predicates are not definable inductively. An example for a proof-relevant non-strictly positive predicate is the interpretation of codes of function types:

data Ty : Set where
  top : Ty
  fun : Ty → Ty → Ty

⟦_⟧ : Ty → Set
⟦ top     ⟧ = ⊤
⟦ fun A B ⟧ = ⟦ A ⟧ → ⟦ B ⟧  -- you can't put this in "data"

Decidable is self-explanatory; A -> Bool functions are necessarily decidable, which makes them unsuitable for predicates which are undecidable or cannot be easily written out as a structural Bool function. The advantage of decidability is excluded middle reasoning, which is not possible with the non-Bool predicate definitions without postulates or additional decidability proofs.

Second, about the practical ramifications in Agda/Idris.

  • You can do dependent pattern matching on proofs of inductive predicates. With recursive and Boolean predicates, you have to pattern match on the A values first, to make predicate witnesses compute. Sometimes this makes inductive predicates convenient, e.g. you could have an enumeration type with 10 constructors, and you want a predicate to hold at only one constructor. The inductively defined predicate lets you only match on the true case, while the other versions require you to match on all cases all the time.

  • On the other hand, Boolean and recursive predicates compute automatically as soon as you know that an A element has a given shape. This can be used in Agda to make type inference fill in proofs automatically, without tactics or instances. For example, a hole or implicit argument with type hastrue xs can be solved by eta rules for pairs and the unit type, whenever xs is a list expression with a known true-containing prefix. This works analogously with Boolean predicates.