So far you listed two ways of defining predicates:
A -> Bool
functions.
- Inductive predicates.
I would add one more:
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.