The Boolean Blindness blog post by Dr. Robert Harper presents a generally excellent point: the use of Booleans is inherently connected with a loss of information, the so-called Boolean Blindness. For example, even if we for some list xs know that the expression all even xs = True holds, we often can not use that information further along in our program1 and we still have to handle the cases where an element is odd, even though such case is impossible in practice.

Fortunately, as we will discuss in this post, it is possible to convert Boolean predicates to constructive predicates using a straightforward almost mechanical recipe. Let us take a simple example like calculating whether a natural number2 is even (or odd). We use an inductive definition which states that zero is even, one is not even, and adding two to any even number will result in an even number (ditto with odd numbers).

even : Nat -> Bool
even Z = True
even (S Z) = False
even (S (S n)) with (even n)
  even (S (S n)) | True = True
  even (S (S n)) | False = False

The process to convert the any Boolean predicate—including even—to a type-level one is as follows: We first create a data-type with the same signature as our function, except replacing each occurance of Bool with Type. For each True case, we create a new uniquely named constructor which is indexed by the functions patterns, and we replace any dependent calls to itself or other predicates with arguments that expect inhabitants of that call. For our even example, the corresponding type-level predicate Even will look as follows:

data Even : Nat -> Type where
  Even_Z  : Even Z
  Even_SS : Even n -> Even (S (S n))

Another example is the less than or equal function lte below. It follows a similar structure to our even example, but this time also has multiple arguments; that is however not an issue since our presented technique generalises for any number of arguments.

lte : Nat -> Nat -> Bool
lte Z m = True
lte (S n) Z = False
lte (S n) (S m) with (n `lte` m)
  lte (S n) (S m) | True = True
  lte (S n) (S m) | False = False

Again we create a datatype with the required type signature, and map each True case to a unique constructor, finally ending up with the type-level Lte predicate:

data Lte : Nat -> Nat -> Type where
  Lte_Z : Lte Z m
  Lte_S : Lte n m -> Lte (S n) (S m)
any : (a -> Bool) -> List a -> Bool
any f [] = False
any f (x :: xs) with (f x)
  any f (x :: xs) | True = True
  any f (x :: xs) | False with (any f xs)
  any f (x :: xs) | False | True = True
  any f (x :: xs) | False | False = False
data Any : (f : a -> Type) -> List a -> Type where
  Any_Here  : f x      -> Any f (x :: xs)
  Any_There : Any f xs -> Any f (x :: xs)

Decisions, Decisions

For most type-level predicates that are converted from Boolean functions (namely those with decidable equality of arguments), it is usually possible to define decision procedures that allow users to regain the ability to decide whether the predicate holds. As can be seen below it follows the style of the Boolean functions, except that we have to be more careful around the branches where the predicate does not hold.

notEven1 : Even 1 -> Void
notEven1 Even_Z impossible
notEven1 (Even_SS n) impossible

notEvenOff : (Even k -> Void) -> Even (S (S k)) -> Void
notEvenOff f (Even_SS x) = f x
decEven : (n : Nat) -> Dec (Even n)
decEven Z = Yes Even_Z
decEven (S Z) = No notEven1
decEven (S (S n)) with (decEven n)
  decEven (S (S n)) | (Yes prf) = Yes (Even_SS prf)
  decEven (S (S n)) | (No contra) = No (notEvenOff contra)
notLteZ : Lte (S k) 0 -> Void
notLteZ Lte_Z impossible
notLteZ (Lte_S l) impossible

notLteOff : (Lte n m -> Void) -> Lte (S n) (S m) -> Void
notLteOff f (Lte_S x) = f x
decLte : (n : Nat) -> (m : Nat) -> Dec (Lte n m)
decLte Z m = Yes Lte_Z
decLte (S n) Z = No notLteZ
decLte (S n) (S m) with (decLte n m)
  decLte (S n) (S m) | (Yes prf) = Yes (Lte_S prf)
  decLte (S n) (S m) | (No contra) = No (notLteOff contra)
notAnyEmpty : ((x : a) -> Dec (f x)) -> Any f [] -> Void
notAnyEmpty f (Any_Here x) impossible
notAnyEmpty f (Any_There xs) impossible

notAnyOff : (f x -> Void) -> (Any f xs -> Void) -> Any f (x :: xs) -> Void
notAnyOff notf notany (Any_Here x) = notf x
notAnyOff notf notany (Any_There x) = notany x
decAny : ((x : a) -> Dec (f x)) -> (xs : List a) -> Dec (Any f xs)
decAny decf [] = No (notAnyEmpty decf)
decAny decf (x :: xs) with (decf x)
  decAny decf (x :: xs) | (Yes prf) = Yes (Any_Here prf)
  decAny decf (x :: xs) | (No contra) with (decAny decf xs)
    decAny decf (x :: xs) | (No contra) | (Yes prf) = Yes (Any_There prf)
    decAny decf (x :: xs) | (No contra) | (No contra') =  No (notAnyOff contra contra')

1: In most mainstream functional languages, like Haskell, Standard ML, F#, Racket, Scheme, etc.

2: Defined as data Nat = Z | S Nat

Blog Logo

Ahmad Salim Al-Sibahi



Ahmad Salim Al-Sibahi

"Currently working on: Validating and verifiying high-level transformations using traditional programming language techniques"

Back to Overview