Check true.
Check false.
true
: bool
false
: bool
Logic and Functional Programming
Florent Schaffhauser
Heidelberg University
(Summer 2026)
This note introduces Lambda Calculus, basics of Rocq function definitions, Boolean values in Rocq, basic pattern matching, curried functions, and the first small proofs about Boolean functions. The original lecture file was a Rocq source file; here the surrounding comments have been turned into prose, and Rocq commands are placed in executable Quarto code blocks.
In lambda calculus, the basic syntactic forms are:
So the two central operations are:
These can be seen as complementary operations. For example:
\[ \labs{x}{x\cdot 2 + 1} \]
is the function \(x \to x \cdot 2 + 1\), and applying it to 3 gives:
\[ \begin{aligned} \lapp{(\labs{x}{x\cdot 2 + 1})}{3} &\to 3 \cdot 2 + 1 &&\text{($\to$ denotes reduction)}\\ &\to 7 \end{aligned} \]
This reduction rule is called \(\beta\)-reduction:
Definition 3.1 (\(\beta\)-reduction) \[ \lapp{\labs{x}{t}}{a} \betared t[x := a] \]
It says: applying a lambda abstraction to an argument means substituting the argument for the bound variable in the body.
In a term like:
\[ \labs{x}{x\cdot 2 + 1} \]
The variable \(x\) is bound the \(\lambda\). Its scope is the body \(x\cdot 2 + 1\).
In constrast consider:
\[ \labs{x}{x\cdot y + 1} \]
Here \(x\) is bound and \(y\) is not. The variable \(y\) is free. Nevertheless this term is still syntactically meaningful. It represents a function of \(x\), but one that depends on an external parameter \(y\). For example, consider the reduction:
\[ \lapp{(\labs{x}{x\cdot y + 1})}{3} \betared 3\cdot y + 1 \]
The result still contains the free variable \(y\), and this term is not automatically interpreted as a function of two variables. For a function with \(x\) and \(y\), we must bind both variables explicitly:
\[ \labs{x}{\labs{y}{x \cdot y + 1}} \]
This is a curried two argument function: It first takes \(x\), then returns a function that takes \(y\). Consider the following reduction:
\[ \begin{aligned} \lapp{(\lapp{(\labs{x}{\labs{y}{x\cdot y + 1}})}{3})}{4} &\betared \lapp{(\labs{y}{3\cdot y + 1})}{4} \\ &\betared 3 \cdot 4 + 1 \\ &\betared 13 \end{aligned} \]
In typed lambda calculus, variables and functions have types.
\[ \tlabs{x}{\Nat}{x \cdot 2 + 1} : \Nat \to \Nat \]
A two-argument function can be written as:
\[ \tlabs{x}{\Nat}{\tlabs{y}{\Nat}{x\cdot y + 1}} : \Nat \to \Nat \to \Nat \]
By convention, arrow types associate to the right, so
\[ \Nat \to \Nat \to \Nat \]
means:
\[ \Nat \to (\Nat \to \Nat) \]
So a two-argument function is really a function that takes one argument and returns another function. (Curried function notation)
The term
\[ \tlabs{x}{\Nat}{x \cdot y + 1} \]
contains the free variable \(y\). In typed lambda calculus, this term is not typeable in the empty context, because the type of \(y\) is unknown. But it is typeable in a context where \(y : \Nat\) is assumed.
We can write this as
\[ y : \Nat \vdash \tlabs{x}{\Nat}{x \cdot y + 1} : \Nat \to \Nat \]
This means:
\[ \text{Assuming $y$ is a natural number} \\ \tlabs{x}{\Nat}{x \cdot y + 1} \, \text{is a function from $\Nat$ to $\Nat$} \]
By contrast,
\[ \vdash \tlabs{x}{\Nat}{x \cdot 2 + 1} : \Nat \to \Nat \]
has no assumptions. It is a closed term.
In type theory, abstraction and application can be understood as the introduction and elimination forms for function types, analogous to introduction and elimination of implication symbol in natural deduction calculus.
These should be understood as type judgment rules.
Fur a function type \(A \to B\) consider:
Consider the following:
\[ \Gamma, x : A \vdash t : B \]
This judgment means:
Assuming the type judgments in \(\Gamma\) (the context) and that the additional variable \(x\) has type \(A\), then the term \(t\) has the type \(B\) in the same context \(\Gamma\).
The lambda-introduction rule says:
\[ \infrule{ \Gamma, x : A \vdash t : B }{ \Gamma \vdash \labs{x}{t} : A \to B } \]
Meaning:
if \(t\) has type \(B\) under temporary assumptions \(x : A\) and context \(\Gamma\), then \(\labs{x}{t}\) has type \(A \to B\) under the original context \(\Gamma\).
The elimination rule for the function type \(A \to B\) (also called function application and implication elimination):
With context, the rule is written as
\[ \infrule{ \Gamma \vdash f : A \to B \qquad \Gamma \vdash a : A }{ \Gamma \vdash \lapp{f}{a} : B } \]
The first premise
\[ \Gamma \vdash f : A \to B \]
says:
Under the assumptions \(\Gamma\), \(f\) is a function that takes input \(A\) and produces output of type \(B\).
The second premise
\[ \Gamma \vdash a : A \]
says:
Under the same assumptions in \(\Gamma\), \(a\) is a term of type \(A\).
The conclusion
\[ \Gamma \vdash \lapp{f}{a} : B \]
says:
Applying \(f\) to \(a\) gives a term of type \(B\).
Rocq uses they keyword fun for \(\lambda\). So the lambda term:
\[ \tlabs{x}{\Nat}{x \cdot 2 + 1} \]
is written in Rocq as:
fun x : nat => x * 2 + 1
So in general
\[ \tlabs{x}{A}{t} \]
corresponds to
fun x : A => t
Rocq lets us define fucntions in two equivalent styles. For example, Boolean negation can be written in the familiar argument style:
Definition negb(b : bool) : bool :=
match b with
| true => false
| false => true
end.
But it also can be written by explicitly giving a function value:
Definition negb : bool -> bool :=
fun b : bool =>
match b with
| true => false
| false => true
end.
These two definitions are equivalent. So in general
Definition f (x : A) : B :=
body.
Definition f : A -> B :=
fun x : A => body.
are equivalent.
Same idea applies. For example:
Definition g (x y : nat) : nat :=
x * y + 1.
is shorthand for
Definition g : nat -> nat -> nat :=
fun x y : nat => x * y + 1
where fun x y : nat => x * y + 1 is itself a shorthand for:
fun x : nat => fun y : nat => x * y + 1.
Or in lambda calculus notation:
\[ \labs{x\, y}{x * y + 1} \]
is a shorthand for:
\[ \labs{x}{\labs{y}{x * y + 1}} \]
Boolean values are available without any imports in Rocq. There are two canonical Boolean values:
truefalseBoth are values of type bool.
Check true.
Check false.
true
: bool
false
: bool
In Rocq, every expression has a type. The expressions true and false have type bool. The type bool itself also has a type: Rocq classifies it as Set.
Roughly speaking, Set is the universe of computational data types, such as Booleans, natural numbers, lists, and other datatypes. More generally, Set itself lives in a larger universe called Type.
Check bool.
About bool.
Check Set.
Check Type.
bool
: Set
bool : Set
bool is not universe polymorphic
Expands to: Inductive Corelib.Init.Datatypes.bool
Declared in library Corelib.Init.Datatypes, line 39, characters 10-14
Set
: Type
Type
: Type
A useful first picture is:
true : bool
false : bool
bool : Set
Set : Type
So the hierarchy starts with ordinary values, then their types, and then universes classifying those types.
In type theory, types are themselves objects that can be checked and classified. This is why commands like Check bool, Check Set, and Check Type are meaningful.
Certain pre-defined functions are also available out of the box and can be used to compute expressions. For example, negb is Boolean negation.
Check negb.
About negb.
Check negb false.
Compute negb false.
negb
: bool -> bool
negb : bool -> bool
negb is not universe polymorphic
Arguments negb b%bool_scope
negb is transparent
Expands to: Constant Corelib.Init.Datatypes.negb
Declared in library Corelib.Init.Datatypes, line 84, characters 11-15
negb false
: bool
= true
: bool
A function of two variables like orb is written in curried notation. This means that the value of orb on two Boolean arguments is written as
orb true false
not as
orb (true, false)
The second expression would suggest that orb takes a pair as input. But Rocq’s orb takes one Boolean argument first, and then returns a new function waiting for the second Boolean argument.
Check orb true false.
Check orb.
Check bool -> (bool -> bool).
Check orb true.
(true || false)%bool
: bool
orb
: bool -> bool -> bool
bool -> bool -> bool
: Set
orb true
: bool -> bool
The type
bool -> bool -> bool
should be read as
bool -> (bool -> bool)
because arrow types associate to the right. Therefore, orb true is a function of type bool -> bool.
A function type A -> B -> C means A -> (B -> C). A function of two arguments is therefore a function that takes one argument and returns another function.
We can compute using this syntax.
Compute negb (orb true false).
= false
: bool
In the rest of the file, we construct basic functions from bool to bool and from bool to bool -> bool. Most of them are already contained in Rocq’s core library, but redefining them is a good way to learn pattern matching.
To avoid conflicts with Rocq’s core library, we wrap everything in a module called BooleanValues. This is optional here, since we do not intend to use the current file as a library later on.
Module BooleanValues.
Inside the module, our own definitions can have the same names as functions from Rocq’s core library. For example, when we define our own negb, it shadows the library function of the same name inside this module. The original library function still exists, but the unqualified name negb will refer to our local definition.
A module gives us a local namespace. This lets us experiment with names like negb, andb, and orb without permanently overwriting Rocq’s library definitions.
Let us start with the definition of the negb function, which sends true to false and false to true. We define it by pattern matching on Boolean values.
Definition negb (b : bool) : bool :=
match b with
| true => false
| false => true
end.
Check negb.
negb
: bool -> bool
The definition says: to compute negb b, inspect the possible forms of b.
b is true, return false.b is false, return true.Since bool has exactly two constructors, true and false, these two cases are exhaustive.
About negb.
Compute negb false.
negb : bool -> bool
negb is not universe polymorphic
Arguments negb b%bool_scope
negb is transparent
Expands to: Constant Top.BooleanValues.negb
Declared in toplevel input, characters 11-15
= true
: bool
We can also give expressions a name and then use that name in later computations.
Compute negb (negb false).
Definition example_bool := negb false.
Compute negb example_bool.
= false
: bool
= false
: bool
Pattern matching is Rocq’s way of defining functions by cases. For bool, there are exactly two cases: true and false.
Now we define an andb function. It takes two Booleans b and b', and returns their Boolean conjunction.
The truth table for Boolean conjunction is:
b |
b' |
andb b b' |
|---|---|---|
true |
true |
true |
true |
false |
false |
false |
true |
false |
false |
false |
false |
This truth table can be translated directly into a pattern match with four cases.
Definition andb (b b' : bool) : bool :=
match b, b' with
| true, true => true
| true, false => false
| false, true => false
| false, false => false
end.
In fact, some simplifications are possible. If the first Boolean is true, then the result is just the second Boolean. If the first Boolean is false, then the result is always false, regardless of the second Boolean.
match b, b' with
| true, b' => b'
| false, _ => false
end
The symbol _ is a wildcard pattern. It means: there is some value here, but we do not need to name it because the result does not depend on it.
Another possible, but less readable, way is to pattern match first on b and then on b'. This produces a nested program. It typechecks, but it is more verbose. Note that only the final end is followed by a period.
match b with
| true =>
match b' with
| true => true
| false => false
end
| false =>
match b' with
| true => false
| false => false
end
end.
As with negb, we can use andb to compute Boolean values.
Compute andb true false.
Compute andb true (negb example_bool).
= false
: bool
= false
: bool
Recall that andb is a function of two variables, but it is written in curried notation. If we ask Rocq to check its type, we get bool -> bool -> bool, not a function type from pairs.
Check andb.
Check andb true.
Compute andb true.
andb
: bool -> bool -> bool
andb true
: bool -> bool
= fun b' : bool => if b' then true else false
: bool -> bool
The expression andb true is a partially applied function. It is the function that takes a Boolean b' and returns andb true b', which is just b'.
We see in particular that arrow types associate to the right:
bool -> bool -> bool
=
bool -> (bool -> bool)
This means that an expression like
andb b b'
is parsed as
(andb b) b'
In contrast, the arrow type (bool -> bool) -> bool describes a function which takes a Boolean function as input and returns a Boolean value. We can define an example by evaluating a function f : bool -> bool at a given Boolean value.
Definition eval_at_true : (bool -> bool) -> bool :=
fun f => f true.
Compute eval_at_true negb.
Compute eval_at_true (andb true).
Definition eval_at (b : bool) : (bool -> bool) -> bool :=
fun f => f b.
Compute eval_at false negb.
Compute eval_at false (andb true).
= false
: bool
= true
: bool
= true
: bool
= false
: bool
Let us define an orb function on Booleans.
The truth table for Boolean disjunction is:
b |
b' |
orb b b' |
|---|---|---|
true |
true |
true |
true |
false |
true |
false |
true |
true |
false |
false |
false |
If the first Boolean is true, then the result is always true. If the first Boolean is false, then the result is the second Boolean.
Definition orb (b b' : bool) : bool :=
match b, b' with
| true, _ => true
| false, b' => b'
end.
Since the first branch does not actually use b', and the second branch only returns b', we can also write the same idea as a match on b alone.
match b with
| true => true
| false => b'
end
Compute orb false false.
Compute orb false (negb false).
= false
: bool
= true
: bool
Let us define Boolean implication. The intended truth table is:
b |
b' |
implb b b' |
|---|---|---|
true |
true |
true |
true |
false |
false |
false |
true |
true |
false |
false |
true |
This corresponds to the classical idea that P -> Q is false only when P is true and Q is false.
For this one, we use Rocq’s if ... then ... else ... syntax.
Definition implb (b b' : bool) : bool :=
if b then b' else true.
Here
if b then b' else true
is essentially shorthand for the following pattern match:
match b with
| true => b'
| false => true
end
So if ... then ... else ... is not a separate mysterious mechanism. For Booleans, it is a convenient notation for pattern matching on a Boolean condition.
Exercise: implement the function implb using pattern matching. The result of the following computation should be the Boolean true.
Compute implb false true.
= true
: bool
Exercise: implement the Boolean if-then-else function as a function with type signature
bool -> bool -> bool -> bool
It should take three Boolean arguments:
condition -> then_branch -> else_branch -> result
One possible implementation is:
Definition ifb (b x y : bool) : bool :=
match b with
| true => x
| false => y
end.
Next, we start using Rocq as a tool for proving properties of the functions we have defined. The syntax looks different at first, but in type theory stating a theorem is closely related to declaring a type, and proving a theorem is closely related to constructing a term of that type.
For example, the statement
negb (negb true) = true
is an equality proposition. To prove it means to construct a proof object inhabiting that proposition.
In Rocq, a theorem statement is a type. Proving the theorem means constructing a term whose type is the theorem statement.
The first property that we prove is that, for every Boolean value b, we have
negb (negb b) = b
In ordinary language: applying Boolean negation twice gives us back the original Boolean value.
Let us start with some special cases, in which we prove our equality by direct computation. When using an interactive Rocq editor, the tactic state shows the current goal and helps guide the proof.
Theorem negb_negb_true_eq_true : negb (negb true) = true.
Proof.
simpl.
Proving: negb_negb_true_eq_true 1 subgoal -------------- (1/1) true = true
The simpl tactic simplifies the goal by computation. In this case, negb (negb true) computes to true.
reflexivity.
Proving: negb_negb_true_eq_true No more subgoals
The reflexivity tactic proves goals where the two sides of an equality reduce to the same expression. It is stronger than a purely textual check: it can unfold definitions and compute enough to see that both sides are definitionally equal.
Qed.
If you are just getting started with theorem provers, focus here on the block that starts with Proof and ends with Qed. This is a Rocq proof script. It is written in the tactic language, while function definitions like negb and andb are written in the core term language, called Gallina.
In this simple example, simpl is not actually necessary, because reflexivity can perform the needed computation itself.
Theorem negb_negb_false_eq_false : negb (negb false) = false.
Proof.
reflexivity.
Qed.
Here is the same proof written directly as a Gallina term. Note that we use Definition rather than Theorem.
Definition negb_negb_false_eq_false' :
negb (negb false) = false :=
eq_refl.
The term eq_refl is the canonical proof that an expression is equal to itself. Since both sides reduce to the same Boolean, eq_refl is accepted.
You can check that the theorem and the definition produce essentially the same proof object using the Print command.
Print negb_negb_false_eq_false.
Print negb_negb_false_eq_false'.
negb_negb_false_eq_false = eq_refl
: negb (negb false) = false
negb_negb_false_eq_false' = eq_refl
: negb (negb false) = false
If you try to prove something incorrect, the prover will let you know.
Theorem fail_to_unify : negb (negb false) = true.
Proof.
Fail reflexivity.
Admitted.
The command has indeed failed with message: Unable to unify "true" with "negb (negb false)".
The command Fail reflexivity. asks Rocq to check that reflexivity fails. This is useful for experimentation. The theorem is then closed with Admitted, which means that the statement is accepted without proof.
Admitted is not a real proof. It is useful while experimenting or developing a file, but admitted theorems should not be treated as established results in finished work.
Now let us prove the general theorem. We need to prove the statement for an arbitrary Boolean b. Since b can only be true or false, it is natural to split the proof into two cases.
The destruct tactic plays the same role in proofs that the match keyword plays in function definitions.
match b with ... end defines a result by considering all possible forms of b.destruct b proves the current goal by considering all possible forms of b.Since bool has exactly two constructors, destruct b creates two subgoals: one for b = true and one for b = false.
Theorem negb_inv (b : bool) : negb (negb b) = b.
Proof.
destruct b.
Proving: negb_inv 2 subgoals -------------- (1/2) negb (negb true) = true -------------- (2/2) negb (negb false) = false
After destruct b, the goal has been modified. In the first case, b has been replaced by true; in the second case, b has been replaced by false.
We use focusing dashes to separate the two subgoals.
- simpl. reflexivity.
Proving: negb_inv This subproof is complete, but there are some unfocused goals: -------------- (1/1) negb (negb false) = false
The second case can be solved by reflexivity alone.
- reflexivity.
Instead of using reflexivity, we can also reuse a previous proof with the exact tactic. The theorem negb_negb_false_eq_false has exactly the type needed for the second goal.
- exact negb_negb_false_eq_false.
Qed.
One may observe that negb_inv true is a proof of the equality
negb (negb true) = true
and that negb_inv itself is a proof of the universally quantified proposition
forall b : bool, negb (negb b) = b
Check negb_inv true.
Check negb_inv.
negb_inv true
: negb (negb true) = true
negb_inv
: forall b : bool, negb (negb b) = b
A theorem with a variable, such as Theorem negb_inv (b : bool) : ..., behaves like a function that takes an argument b : bool and returns a proof of the corresponding statement.
Recall the formulas from classical logic:
(P ⇒ Q) ⇔ ¬P ∨ Q
(P ⇒ Q) ⇔ ¬(P ∧ ¬Q)
The first formula is often taken as a definition of implication in classical logic.
In this section, we interpret the logical connectives as Boolean functions:
| Logical notation | Boolean function |
|---|---|
P ⇒ Q |
implb b b' |
¬P |
negb b |
P ∨ Q |
orb b b' |
P ∧ Q |
andb b b' |
Since we are working with Boolean-valued functions, saying that two Boolean formulas are equivalent means saying that they compute to the same Boolean value for all inputs. Therefore, we express equivalence here using equality of Booleans.
Later, when working directly with propositions in Prop, logical equivalence will be expressed differently, for example using <->.
We first prove the Boolean version of
(P ⇒ Q) ⇔ ¬P ∨ Q
This exercise also shows how to structure a proof with pattern matching on several variables. First, we present a nested version: we destruct b, and then destruct b' in each branch. This gives two cases corresponding to the possible values of b, with two subcases corresponding to the possible values of b'.
Theorem implb_eq_orb_negb (b b' : bool) :
implb b b' = orb (negb b) b'.
Proof.
destruct b.
- destruct b'.
+ reflexivity.
+ reflexivity.
- destruct b'.
all: reflexivity.
Qed.
The tactic all: reflexivity applies reflexivity to all remaining subgoals. Here, after destructing b', both remaining cases can be solved in the same way.
Next, we prove the Boolean version of
(P ⇒ Q) ⇔ ¬(P ∧ ¬Q)
Here we destruct b and b' simultaneously. This immediately creates four cases, much like the simultaneous pattern matching used in the definition of andb.
Theorem implb_eq_negb_andb_negb (b b' : bool) :
implb b b' = negb (andb b (negb b')).
Proof.
destruct b, b'.
all: reflexivity.
Qed.
Notice that we used explicit parameters (b b' : bool) rather than implicit parameters {b b' : bool}. Braces introduce implicit arguments, which Rocq tries to infer from context. This is useful later, but explicit parameters are clearer at this introductory stage.
End BooleanValues.
In Rocq, there is a deep analogy between ordinary functions and theorem with parameters. This is an instance of the Curry-Howard correspondence:
Propositions behave like types Proofs behave like terms/values inhabiting those types
For example bool is a type, and its inhabitants are the Boolean values:
true : bool
false : bool
Similarly, a proposition such as
neg (neg b) = b
can be understood as a type. Its inhabitants are proofs of that proposition. So if we have
p : negb (negb b) = b
Then p is a proof object whose type is the proposition
negb (negb b) = b
Consider the ordinary boolean function:
Definition f (b : bool) : bool :=
negb b.
This says:
ftakes an inputb : booland returns an output of typebool
it’s type is
f : bool -> bool
And compare with the theorem:
Theorem negb_inv (b : bool) : negb (negb b) = b.
This can be understood as saying:
negb_invtakesb : booland returns a proof ofnegb (negb b) = b.
After the theorem is proved, Rocq gives it the type:
negb_inv : forall b : bool, negb (negb b) = b
This is a dependent function type. It is called dependent becaus the result type depends on the input b. For example, if we apply negb_inv to true, we get:
negb_inv true : negb (negb true) = true
If we apply it to false, we get:
negb_inv false : negb (negb false) = false.
The important point is that negb_inv true is not a Boolean value. It is a proof object. More precisely, it s a proof of the proposition.
negb (negb true) = true
We can compare theorems and functions as follows:
| Expression | Input | Output |
|---|---|---|
f |
b : bool |
negb b : bool |
negb_inv |
b : bool |
proof of negb (negb b) = b |
The theorem
Theorem negb_inv (b : bool) : negb (negb b) = b.
Proof.
destruct b.
- reflexivity.
- reflexivity.
Qed.
is conceptually similar to:
Definition negb_inv : forall b : bool, negb (negb b) = b :=
fun b =>
match b with
| true => eq_refl
| false => eq_refl
end.
The type bool lives in the universe Set:
Check bool.
bool
: Set
whereas propositions such as equalities live in Prop:
Check negb (negb true) = true.
negb (negb true) = true
: Prop
Both are type-like objects, but they have different roles:
Set: computational objects
Prop: logical propositions / proof types
DestructThe tactic destruct performs case analysis on an object according to the possible ways that object could have been constructed.
For example, if
b : bool
then b has only two possible constructors:
true : bool
false : bool
So when we write
destruct b.
Rocq splits the proof into two cases:
Case 1:
b = true
Case 2:b = false
The word destruct is therefore connected to the idea of deconstructing a value: we inspect its outermost constructor and split into the corresponding cases.
The same idea applies to other inductive types. For natural numbers, a value of type nat is constructed either as 0 or as the successor of antoher natural nubmer. Therefore destruct n. splits the proof into the cases
Case 1: n = 0
Case 2: n = S n’
This is closely related to pattern matching. In a function definition, we write:
match b with
| true => ...
| false => ...
end.
In a proof, we write:
destruct b.
The difference is:
match: case-splits inside a term/program
destruct: case-splits inside an interactive proof
In this lecture, we saw the following ideas:
true and false are the two canonical values of type bool.bool is classified by the universe Set._ means that the value is irrelevant in that branch.if ... then ... else ... is a convenient notation for pattern matching on Booleans.simpl computes in the goal, while reflexivity proves equalities whose two sides reduce to the same expression.destruct performs case distinction in proofs, analogous to match in programs.