4  A toy implementation of propositional formulas

To be able to use Rocq files that contain Unicode characters like

ℕ, ∧, ¬ or

in our code, we add support for UTF8. The notation ℕ must be defined manually.

From Stdlib Require Import Utf8.

Definition:= nat.

To input Unicode characters in VS Code/Codium, you can use extensions such as latex-input or Unicode shortcuts. In the online scratchpad, ASCII encoding like -> will render as on the screen, but if you copy-paste your code in a text file, you will see -> again.

In the Rocq scratchpad, use the following block instead.

From Coq Require Import Utf8.

Definition:= nat.

4.1 Well-formed formulas

Recall that the collection of well-formed formulas is the datatype Wff defined inductively as follows:

  • Every so-called basic proposition P₀, P₁, P₂, ... is a well-formed formula.
  • If F and F' are well-formed formulas and is one of the symbols ⇒, ∧, ∨, then F ♢ F' is a well-formed formula.
  • If F is a well-formed formula, then ¬F is a well-formed formula.

We now declare the type of well-formed formulas in Rocq. Take a good look at the syntax of this declaration and compare it to the informal definition above.

Inductive Wff :=
  | P    : ℕ   → Wff
  | Neg  : Wff → Wff
  | Impl : Wff → Wff → Wff
  | Conj : Wff → Wff → Wff
  | Disj : Wff → Wff → Wff.

This in particular defines functions

P    : ℕ → Wff 
Neg  : Wff → Wff
Impl : Wff → Wff → Wff
Conj : Wff → Wff → Wff
Disj : Wff → Wff → Wff

called constructors, all of whose codomain is Wff.

Check P.                            (* P : ℕ → Wff *)
Check Neg.                          (* P : Wff → Wff *)
Check Impl.                         (* P : Wff → Wff → Wff *)

Per our declaration, basic propositions are indexed by natural numbers and type-check as well-formed formulas.

Check P 0.                          (* P 0  : Wff *)
Check P 42.                         (* P 42 : Wff *)

We can compose constructors to construct new well-formed formulas out of old ones. The resulting expressions not only typecheck but can also be simplified automatically by the typechecker, using the keyword Compute.

Check Neg (P 0).                    (* Neg (P 0) : Wff *)

Check Impl (P 1) (P 2).             (* Impl (P 1) (P 2) : Wff *)
Check Conj (P 1) (P 2).             (* Conj (P 1) (P 2) : Wff *)

Definition F:= Neg (P 0).
Definition F:= Conj (P 1) (P 2).  

Check F₁.                           (* F₁ : Wff *)
Check F₂.                           (* F₂ : Wff *)

Compute F₁.                         (* = Neg (P 0) *)
Compute F₂.                         (* = Conj (P 1) (P 2) *)

Check Impl F₁ F₂.                   (* Impl F₁ F₂ : Wff *)
Compute Impl F₁ F₂.                 (* = Impl (Neg (P 0)) (Conj (P 1) (P 2)) *)

Note that Impl, Conj and Disj are functions of two variables but they are written in “curried notation”. This means that Imp, for instance, is not defined on the Cartesian productWff × Wff, but instead sends a given well formed formula F to a function Imp F : Wff → Wff.

Section Scratch.
  
  Variable (dummy_formula : Wff).
  Check Impl dummy_formula.         (* Impl dummy_formula : Wff → Wff *)

End Scratch.

As a side remark, note that Roq recognises Wff as something called Set. In the declaration, we could have written Inductive Wff : Set := but this is not necessary; it is inferred by the type checker.

Check Wff                           (* Wff : Set *)

4.2 Notation

For greater convenience, let us implement infix notation for the binary logical connectives. The associativity rules we set are consistent with the ones in Rocq’s Corelib.Init.Notations.

Notation "a ∧ b" := (Conj a b) (at level 80, right associativity).
Notation "a ∨ b" := (Disj a b) (at level 85, right associativity).
Notation "a ⇒ b" := (Impl a b) (at level 99, right associativity).

With these notations, we get the associativity rules that we talked about in the lecture, where expressions such as P 1 ⇒ P 2 ⇒ P 3 are parsed as P 1 ⇒ (P 2 ⇒ P 3). Note that we do not need brackets around P 1 or P 2 (functions bind tighter that anything else, so to speak).

Check P 1 ⇒ P 2 ⇒ P 3.              (* P 1 ⇒ P 2 ⇒ P 3   : Wff *)
Check P 1 ⇒ (P 2 ⇒ P 3).            (* P 1 ⇒ P 2 ⇒ P 3   : Wff *)
Check (P 1 ⇒ P 2) ⇒ P 3.            (* (P 1 ⇒ P 2) ⇒ P 3 : Wff *)

Check P 1 ∧ P 2 ∧ P 3.              (* P 1 ∧ P 2 ∧ P 3   : Wff *)
Check P 1 ∧ (P 2 ∧ P 3).            (* P 1 ∧ P 2 ∧ P 3   : Wff *)
Check (P 1 ∧ P 2) ∧ P 3.            (* (P 1 ∧ P 2) ∧ P 3 : Wff *)

Since has been set at so-called “level” 80 and has been set at level 85, the binary operator binds tighter than . This is an arbitrary convention, but it is common (just like * usually binds tighter than + when the two pieces of notation are used together).

In practice, this means that P 1 ∧ P 2 ∨ P 3 is parsed as (P 1 ∧ P 2) ∨ P 3, not P 1 ∧ (P 2 ∨ P 3), and similarly for P 1 ∨ P 2 ∧ P 3.

Check (P 1 ∧ P 2) ∨ P 3.            (* P 1 ∧ P 2 ∨ P 3   : Wff *)
Check P 1 ∧ (P 2 ∨ P 3).            (* P 1 ∧ (P 2 ∨ P 3) : Wff *)
Check P 1 ∧ P 2 ∨ P 3.              (* P 1 ∧ P 2 ∨ P 3   : Wff *)

It may be helpful to think of Rocq’s levels as a measure of distance between the operator and its arguments: the shorter the distance, the higher the precedence. For instance, in the expression P 1 ∧ P 2 ∧ P 3, the distance between and its arguments is set to 80, while that of is set to 85. So P 2 is closer to than it is to and the expression is parsed as (P 1 ∧ P 2) ∨ P 3.

Similar considerations apply with , which binds looser than , hence also than .

Check F₁ ⇒ ((F₂ ⇒ P 42) ∨ P 3).     (* F₁ ⇒ (F₂ ⇒ P 42) ∨ P 3 : Wff *)
Check F₁ ⇒ ((F₂ ⇒ P 42) ∧ P 3).     (* F₁ ⇒ (F₂ ⇒ P 42) ∧ P 3 : Wff *)

Note that Rocq does not require blank spaces around the operators, but that we usually insert them.

Check F₁∧F₂⇒F₁.                     (* F₁ ∧ F₂ ⇒ F₁ : Wff *)

Similarly for Neg, we can introduce a convenient notation. No associativity rule here (Neg is not a binary operator), but a formatting rule to guarantee that the pretty printer shows ¬P 0 instead of ¬ P 0 (try it!).

Notation "¬ a" := (Neg a) (at level 75, format "¬ a").

Check ¬P 0.                         (* ¬P 0 : Wff *)
Compute F₁.                         (* = ¬P 0 : Wff *)
Compute F₁ ⇒ F₂.                    (* = ¬P 0 ⇒ P 1 ∧ P 2 *)

Note that, in the scratchpad file available for download, the character ! is used, instead of ¬, to denote the constructor Neg.

Notation "! a" := (Neg a) (at level 75, format "! a").
Check !P 0.

4.3 The height function

When we constructed functions out of the type of Booleans, we did so by pattern matching. We can do that because Booleans are inductively declared in Rocq. More precisely, here is the declaration, as found in Rocq’s Corelib.Init.Datatypes.

Inductive bool : Set :=
  | true  : bool
  | false : bool

The intuition is that, in order to define a function f : bool → X (where X is an arbitrary set/type), it suffices to specify f true and f false as elements/terms of X. Similarly, we should be able to define a function f : Wff → X by defining it on each constructor of Wff.

Let us for instance declare the height function on well-formed formulas that we introduced in Lecture 2. Two observations are in order:

  • A cosmetic one: we can use the notation for Neg, And, etc as valid syntax in the pattern matching, which makes the code more readable.
  • A fundamental one: our height function is defined recursively (it calls upon itself in three out of four subcases), which forces us, in Rocq, to replace the keyword Definition with Fixpoint.
Fixpoint ht (F : Wff) :=
  match F with
  | P i    => 0
  | ¬F     => 1 + ht F
  | F ∧ F' => 1 + max (ht F) (ht F')
  | F ∨ F' => 1 + max (ht F) (ht F')
  | F ⇒ F' => 1 + max (ht F) (ht F')
  end.

Note that the return type of the height function is inferred by the type checker (because the first case returns 0 which is parsed as a natural number by the type checker).

Check ht.                           (* ht : Wff → ℕ *)

Then not only does this typecheck but it also computes 🎉.

Check   ht ((P 1 ∧ P 2) ∨ ¬¬P 3).   (* ht ((P 1 ∧ P 2) ∨ ¬¬P 3) *)
Compute ht (P 1 ∧ P 2 ∨ ¬¬P 3).     (* = 3 *)

With Fixpoint, the syntax

Fixpoint ht : Wff → ℕ := 
  fun F => match F with

is not accepted by the typechecker.

If you want to desugar the Fixpoint syntax, you can write the same program as follows. Also note that the last three cases in the pattern matching are treated in one go, which is a syntax you can of course use elsewhere, too!

Definition ht₀ : Wff → ℕ :=
  fix ht₀ (F : Wff) : nat :=
    match F with
    | P _                      => 0
    | ¬F                       => 1 + ht F
    | F ⇒ F' | F ∧ F' | F ∨ F' => 1 + max (ht F) (ht F')
    end.

We can check that the two functions ht and ht₀ are indeed equal. This is a proof by reflexivity, which implies that the two functions take the same values on every well-formed formula.

Goal ht = ht₀.
Proof.
  reflexivity.
Qed.

It is possible to use Rocq’s tactic language to define the above height function. Namely, one can use the induction tactic. It is worth taking a look at, but in practice I recommend using the Fixpoint construction. Note that the induction tactic can introduce the five sub-cases automatically, as well as name the relevant terms and induction hypotheses in each case, but you can also choose your own names, by replacing

induction F.

by

induction F as [n | F htF | F htF F' htF' | F htF F' htF' | F htF F' htF'].

which nonetheless seems to break the proof by reflexivity that ht = ht₁.

Definition ht₁ (F : Wff) : ℕ.
Proof.
  induction F 
    (* as [n | F htF | F htF F' htF' | F htF F' htF' | F htF F' htF'] *).
  - exact 0.
  - exact (1 + IHF).                (* exact (1 + htF). *)
  - exact (1 + (max IHF1 IHF2)).    (* exact (1 + (max htF htF')). *)
  - exact (1 + (max IHF1 IHF2)).    (* exact (1 + (max htF htF')). *)
  - exact (1 + (max IHF1 IHF2)).    (* exact (1 + (max htF htF')). *)
Defined.
(* Qed *)

Compute ht₁ (P 1 ∧ P 2 ∨ ¬¬P 3).    (* = 3 *)

Goal ht = ht₁.
Proof.
  reflexivity.
Qed.

Careful! If we use Qed instead of Defined in the above construction of ht₁, then the definition becomes opaque and the computation does not return 3 (try it!). It just prints out the expression again…

Note that if you do not use focusing points, you can also write the proof as follows. I personally find this less informative.

Definition ht₂ (F : Wff) : ℕ.
Proof.
  induction F.
  exact 0.
  exact (1 + IHF).
  all: exact (1 + (max IHF1 IHF2)).
Defined.

Compute ht₂ (P 1 ∧ P 2 ∨ ¬¬P 3).    (* = 3 *)

Goal ht = ht₂.
Proof.
  reflexivity.
Qed.

4.4 Subformulas

We will require Rocq’s standard library on linked lists. More precisely, linked lists are defined in Rocq’s Corelib.Init.Datatype but the notation we will be using is defined in the Stdlib.Lists.List. Let us first analyse the definition of lists.

Inductive list (A : Type) : Type :=
  | nil  : list A
  | cons : A → list A → list A.

Arguments nil {A}.
Arguments cons {A} a l.

It is quite general. We see that it depends on a parameter A which is a type. This enables us to consider lists of natural numbers, lists of booleans, etc. For some reason, Rocq recognises these as sets rather than types.

Check list ℕ.                       (* list ℕ : Set *)
Check list bool.                    (* list bool : Set *)

Once a type A is fixed, the type list A is defined inductively, using two constructors, nil {A} : list A and cons {A} : A → list A → list A. Because of the Arguments command placed after the definition, the parameter A is implicit when using nil and cons.

Check nil.                          (* nil : list ?A *)
Check cons.                         (* cons : ?A → list ?A → list ?A *)

Check @nil.                         (* nil : ∀ A : Type, list A *)
Check @cons.                        (* cons : ∀ A : Type, A → list A → list A *)

Check @nil bool.                    (* nil : list bool *)
Check @cons ℕ.                      (* cons : ℕ → list ℕ → list ℕ *)

In general, this implicit parameter can be inferred by the typechecker, as we shall see below when we construct our first examples of lists.

Check (nil : list ℕ).               (* nil : list ℕ *)
Check cons true (cons false nil).   (* cons true (cons false nil) : list bool)

The syntax with cons and nil is hard to parse. This is why cons is replaced by the following infix notation.

Infix "::" := cons (at level 60, right associativity) : list_scope.

To access it, we can either put the notation in scope explicitly, or import a module that opens it (see below).

Open Scope list_scope.
Check 1 :: 2 :: 3 :: nil.           (* 1 :: 2 :: 3 :: nil : list nat *)

This is better, but not yet optimal in practice. So we import the following module.

From Stdlib Require Import List.
Import ListNotations.

In the Rocq scratchpad, use the following block instead.

From Coq Require Import List.
Import ListNotations. 

This makes the pretty printer view of lists much nicer. And more importantly, we can use that same syntax to denote lists in our code, in a variety of ways.

Check 1 :: 2 :: 3 :: nil.           (* [1; 2; 3] : list nat *)
Check [1; 2; 3].                    (* [1; 2; 3] : list nat *)
Check 1 :: [2; 3].                  (* [1; 2; 3] : list nat *)
Check [42].                         (* [42] : list nat *)

Let us now use lists to implement the function strict_sf seen in Lecture 2. Here, we do this using the constructors actual names, not the notation we introduced for them, but you can change Neg F to ¬F, Conj F F' to F ∧ F' etc. Note that the order in which you enter the constructor does not matter for the pattern matching, and that you can use the notation F in the pattern Neg F etc, even though you are pattern matching on a parameter called F in the body of the function.

Fixpoint strict_sf (F : Wff) : list Wff :=
  match F with 
  | P i       => nil
  | Neg F     => F :: strict_sf F
  | Conj F F' => (F :: strict_sf F) ++ (F' :: strict_sf F')
  | Disj F F' => (F :: strict_sf F) ++ (F' :: strict_sf F')
  | Impl F F' => (cons F (strict_sf F)) ++ (cons F' (strict_sf F'))
  end.

The resulting function returns a list of the strict subformulas of a given formula, as expected. If you remove list Wff from the declaration of strict_sf, the return type will be inferred by the type checker, because of the syntax F :: _ in the second case, for F of type Wff (try it!).

Check   strict_sf.                  (* strict_sf : Wff → Wff *)
Compute strict_sf ((P 1 ∧ P 2) ∨ ¬¬P 3).
  (* = [P 1 ∧ P 2; P 1; P 2; ¬¬P 3; ¬P 3; P 3] *)

We can then define the subformula function as in Lecture 2.

Definition sf (F : Wff) : list Wff :=
F :: (strict_sf F).

Compute sf ((P 1 ∧ P 2) ∨ ¬¬P 3).
  (* = [P 1 ∧ P 2 ∨ ¬¬P 3; P 1 ∧ P 2; P 1; P 2; ¬¬P 3; ¬P 3; P 3])

4.5 Substitutions

Given a well-formed formula F, a subset I ⊂ ℕ and a family of well-formed formulas G : ℕ → Wff, we have seen in the lecture how to replace the atomic formulas P n by G n in F, for all n ∈ I. Let us now implement this function in practice.

Fixpoint subst_Wff (F : Wff) (I : ℕ → bool) (G : ℕ → Wff) : Wff :=
  match F with 
  | P j     => if I j then G j else P j
  | ¬F      => ¬(subst_Wff F I G)
  | F₁ ⇒ F₂ => (subst_Wff F₁ I G) ⇒ ((subst_Wff F₂ I G))
  | F₁ ∧ F₂ => (subst_Wff F₁ I G) ∧ ((subst_Wff F₂ I G))
  | F₁ ∨ F₂ => (subst_Wff F₁ I G) ∨ ((subst_Wff F₂ I G)) 
  end.

For the sake of simplicity, we will use the same characteristic function I : ℕ → bool in all our examples. Note how we define I using pattern matching when we want it to represent the finite subset {0; 26; 41; 42} of the set .

Definition I (n : ℕ) : bool :=
  match n with
  | 0  => true
  | 26 => true
  | 41 => true
  | 42 => true
  | _  => false
  end.

4.5.1 Example 1

Let us define a family of formulas to be used when performing the substitution of certain atomic formulas. In this example G₁ n is different from P n for all n, but in the substitution, only those G n for which I n = true will matter.

Definition G₁ : ℕ → Wff :=
  fun n => P (n + 2) ∧ P (n + 1).

In the example below, every atomic subformula P j for which I j = true (so, everything except 25) is replaced by something new, because G₁ j ≠ P j for all ij. Note thatI 41 = truebut that this plays no role becauseP 41does not appear in our example forF`.

Compute subst_Wff (P 0 ∨ P 26 ⇒ P 42 ∧ P 25) I G₁.
  (* = P 2 ∧ P 1 ∨ P 28 ∧ P 27 ⇒ (P 44 ∧ P 43) ∧ P 25 *)

4.5.2 Example 2

We can also define a family of formulas in a way similar to what we did for I, meaning that we only care about defining G₂ n for a finite number of n and for all other n we simply set G n = P n.

Definition G₂ : ℕ → Wff :=
  fun n => 
    match n with
    | 25  => P 44
    | 26  => P 1 ⇒ P 2
    | _   => P n 
    end.

In the example below, only P 26 is replaced by G 26 (even though G 2025 ≠ P 2025 and P 2025 appears in our example for F), because I 2025 ≠ true.

Compute subst_Wff (P 0 ∨ P 26 ⇒ P 42 ∧ P 2025) I G₂.
  (* =  P 0 ∨ (P 1 ⇒ P 2) ⇒ P 42 ∧ P 25 *)

4.5.3 Example 3

For the third example, we define a family of formulas using a conditional statement. The point is to show an example where G n ≠ P n for an infinite quantity of n but also G n = P n for an infinite quantity of n. We will do so by defining the characteristic function of even numbers.

From Corelib Require Import Init.Nat.

In the Rocq scratchpad, use the following block instead.

From Coq Require PeanoNat.
Import Nat.

Back to the main definition

Definition is_even (n : nat) : bool :=
  eqb (n mod 2) 0.

Definition G₃ (n : ℕ) : Wff :=
  if is_even n then P (n / 2) else P n.

In the example below, an atomic formula P j is replaced by G (j / 2) if and only if I j = true and is_even j = true.

Compute subst_Wff (P 0 ∨ P 26 ⇒ P 41 ∧ P 2025) I G₃.
  (* = P 0 ∨ P 13 ⇒ P 41 ∧ P 25 *)

4.5.4 Substitutions in indexed families

The definition of the function subst_Wff suggests a general method to substitute terms in a family P : Y → X (family of terms of type X indexed by the type Y). All you need is a subtype of Y (represented by a characteristic function I : Y → X) and a new family G : Y → X, indexed by the same type Y.

Then for all element y of Y, if y belongs to the subset I (which means that the characteristic function I evaluates to true on y), one replaces P y with G y, and if y does not belong to I, then one leaves P y unchanged.

Definition substFamily {Y} {X} : (Y → X) → (Y → bool) → (Y → X) → Y → X :=
  fun P I G y => if I y then G y else P y.

One can then redefine subst_Wff as follows and check that it behaves as expected on our examples. This time we place (F : Wff) last, so subst_Wff' I G : Wff → Wff.

Fixpoint subst_Wff' (I : ℕ → bool) (G : ℕ → Wff) (F : Wff) : Wff :=
  match F with 
  | P j     => substFamily P I G j (* if I j then G P I j else P j *)
  | ¬F      => ¬(subst_Wff' I G F)
  | F₁ ⇒ F₂ => (subst_Wff' I G F₁) ⇒ ((subst_Wff' I G F₂))
  | F₁ ∧ F₂ => (subst_Wff' I G F₁) ∧ ((subst_Wff' I G  F₂))
  | F₁ ∨ F₂ => (subst_Wff' I G F₁) ∨ ((subst_Wff' I G F₂))
  end.

Compute subst_Wff  (P 0 ∨ P 26 ⇒ P 42 ∧ P 2025) I G₁.
  (* = P 2 ∧ P 1 ∨ P 28 ∧ P 27 ⇒ (P 44 ∧ P 43) ∧ P 25 *)
Compute subst_Wff' I G₁ (P 0 ∨ P 26 ⇒ P 42 ∧ P 2025).
  (* = P 2 ∧ P 1 ∨ P 28 ∧ P 27 ⇒ (P 44 ∧ P 43) ∧ P 25 *)

Compute subst_Wff  (P 0 ∨ P 26 ⇒ P 42 ∧ P 2025) I G₂.
  (* = P 0 ∨ (P 1 ⇒ P 2) ⇒ P 42 ∧ P 25 *)
Compute subst_Wff' I G₂ (P 0 ∨ P 26 ⇒ P 42 ∧ P 2025).
  (* = P 0 ∨ (P 1 ⇒ P 2) ⇒ P 42 ∧ P 25 *)

Compute subst_Wff  (P 0 ∨ P 26 ⇒ P 42 ∧ P 2025) I G₃.
  (* = P 0 ∨ P 13 ⇒ P 21 ∧ P 25 *)
Compute subst_Wff' I G₃ (P 0 ∨ P 26 ⇒ P 42 ∧ P 25).
  (* = P 0 ∨ P 13 ⇒ P 21 ∧ P 25 *)

4.6 Exercise

As an exercise to practice the techniques seen in this file, add one more constructor

Iff : Wff → Wff → Wff

to the type of well-formed formulas, then introduce the (non-associative) infix notation

`⇔` (or `<=>`) 

for it, and complete the definitions of ht, strict_sf and subst_Wff accordingly.

Be careful with the precedence level for . In Rocq’s notation for propositional connectives, the formulas P 1 ⇒ P 2 ⇔ P 3 and P 2 ⇔ P 3 ⇒ P 1 parse respectively as P 1 ⇒ (P 2 ⇔ P 3) and (P 2 ⇔ P 3) ⇒ P 1.