UNDER CONSTRUCTION
Code for this chapter
Foundations in Lean
by Eric Klavins
The notes presented here were initially developed for a special topics graudate course on Lean I taught in the Winter of 2025. The course was taken mainly by Electrical and Computer Engineers with standard programming experience, some engineering mathematics background, and a variety of different application areas in mind. The main reading for the course included all the standard Lean references and books. These notes are merely a supplement for those excellent resources.
That said, I hope the material presented here presents a unique and useful perspective that arose over several decades of my periodically checking in on proof assistants, becoming enamored for a while, and then moving on to other projects. For me, the alure of theorem provers is the possibility of once and for all connecting the foundations of mathematics (Type Theory in the case of Lean) with the work of practicing mathematicians and engineers. Thus, my presentation focuses on foundations, whereas other resources may focus more on the use of Lean for particular applications.
Every new generation of proof assistant gets a bit closer to realizing the goal of making all of mathematics easily formalized. Of all of the tools available, from Agda to Rocq, none has captured my attention like Lean 4 has. I think it is the combination of tooling in VS Code, the self-boostrapping nature of Lean 4 being written in Lean 4, the simultaneous blossoming of machine learning, and the inspiring community of researchers exploring how to use Lean 4. These taken together seem to be giving Lean considerable momentum.
My hope is that these notes are useful for students wishing to understand the type theoretic foundations of Lean and similar tools. I think such an understanding is useful for a variety of reasons. First, I think learning a computer programming language is aided by an understanding of how the language is executed or intepreted. For a language like C, it is hard to imagine becoming a true expert without understanding assembly language, stacks, memory allocation, and compilation. Just using a C debugger like gdb
requires a fair amount of knowledge about the underlying model of computation. For Lean, the model is the lambda calculus, type checking, and type inference. When Lean doesn't work, students can spend hours trying various incantations to make it do what they want. With some understanding of the underlying model of computation, getting unstuck becomes easier. Second, there is no reason to think that the development of proof checkers is somehow complete with Lean as the final solution to the problems that have plagued such tools for decades. Understanding how Lean works will hopefully inspire someone (perhaps even me) to write their own proof checker someday, using Lean and its foundations as a reference architecture.
Finally, the study of the foundations of mathematics is incredibly rich and interesting in its own right, like topology or number theory. I encourage the interested student to dig deeply into this material and then read the primary literature on Type Theory, to gain at least an appreciation if not a mastery of the wonders that underpin how Lean and its cohort of proof checkers work.
Acknowledgements
I would like to acknowledge the students who took my special topics course offered the Winter of 2025 at the University of Washington. We all learned Lean together. At first, I was a few weeks ahead, and by the end of the course I was a few weeks behind. Much of the material here was developed in response to their questions and ideas.
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
A Tour of Lean 4
Installing Lean
The easiest way to install Lean is to follow the quickstart guide at
You will need first install VS Code:
Then go to View > Extensions
and search for "Lean 4" and install it.
This will put a ∀
in the upper right menu bar of VS Code. From there, you can create a new project, which should install Lean and all of the associated tools.
Lean "Project" Types
With the VS Code Extension, you can install two types of projects:
-
Standalone project. Just the basics.
-
Mathlib project. Includes a huge library of most basic and several advanced areas of mathematics. Choose this if in particular if you want to use real numbers, algebra, sets, matrices, etc.
Despite its size, I recommend starting a Mathlib based project. You never know when you might need something from Mathlib.
Notes:
- Wait for the tool to completely finish before opening or changing anything.
- I don't like the option where it creates a new workspace
- Don't make a new project every time you want to try something out. You will use up all the space on your hard drive. Instead, create a single monolithic project and mkae subdirectores for ideas you want to explore.
Directory Structure
If you create a new project called MyProject
, you will get a whole directory of stuff:
MyProject
.github/
.lake/
MyProject/ <-- put your code here
Basic.lean
.gitignore
MyProject.lean
lake-manifest.json
lakefile.toml
lean-toolchain
README.md
For now, you mainly need to know that the subdirectory with the same name as your project is where you can put your .lean files. It has one in it already, called Basic.lean
. Open this and you can start playing with Lean.
Testing an Installation
Try replacing the code in Basic.lean
with the following:
import Mathlib.Tactic.Linarith
#eval 1+2
example (x y z : ℚ)
(h1 : 2*x < 3*y)
(h2 : -4*x + 2*z < 0)
(h3 : 12*y - 4* z < 0) : False := by
linarith
If it is not open already, open Lean infoview
via the ∀ menu.
- Put your curor over
1+2
. You should see 3 in the messages. - Put your cursor just before
by
you will get some goals. - Rut it after
linarith
you will see "No Goals", since the theorem is proved.
Fancy Characters
You can enter fancy characters in Lean using escape sequences
→ \to
↔ \iff
∀ \forall
∃ \exists
ℕ \N
xᵢ x\_i
Go to
∀ > Documentation > ... Unicode ...
for a complete list.
Type Checking
Lean is based on type theory. This means that every term has a very well defined type. To find the type of an expression, use #check. The result will show up in the Infoview.
#check 1
#check "1"
#check ∃ (x : Nat) , x > 0
#check λ x => x+1
#check (4,5)
#check ℕ × ℕ
#check Type
Evaluation
You can use Lean to evaluate expressions using the #eval command. The result will show up in the Infoview.
#eval 1+1
#eval "hello".append " world"
#eval if 2 > 2 then "the universe has a problem" else "everything is ok"
#eval Nat.Prime 741013183
Proofs
We will go into proofs in great detail next week. For now, know that you can state theorems using the theorem
keyword.
theorem my_amazing_result (p : Prop) : p → p :=
λ h => h
In this expression,
my_amazing_result is the name of the theorem (p : Prop) is an assumption that p is a proposition (true or false statement) p → p is the actual theory := delinates the statement of the theorem from the proof λ h => h (the identity function) is the proof
You can use your theorems to prove other theorems:
theorem a_less_amazing_result : True → True :=
my_amazing_result True
Examples vs Proofs
Results don't have to be named, which is useful for trying things out or when you don't need the result again.
example (p : Prop) : p → p :=
λ h => h
example (p q r : Prop) : (p → q) ∧ (q → r) → (p → r) :=
λ ⟨ hpq, hqr ⟩ hp => hqr (hpq hp)
The Tactic Language and sorry
The examples above use fairly low level Lean expressions to prove statements. Lean provides a very powerful, higher level DSL (domain specific language) for proving. You enter the Tactic DSL using by
.
Proving results uses the super sorry
keyword. Here is an example of Tactics and sorry.
example (p q r : Prop) : (p → q) ∧ (q → r) → (p → r) := by
intro h hp
have hpq := h.left
have hqr := h.right
exact hqr (hpq hp)
which can be built up part by part into
example (p q r : Prop) : (p → q) ∧ (q → r) → (p → r) := by
intro ⟨ hpq, hqr ⟩
intro hp
have hq : q := hpq hp
have hr : r := hqr hq
exact hr
Don't worry if none of this makes sense. We'll go into all the gory details later.
Programming
Lean is also a full-fledged functional programming language. For example, much of Lean is programmed in Lean (and then compiled). That said, the Lean Programming Language is not really general purpose: You would probably lose your mind trying to build an operating system with Lean. Rather, Lean is a programming language designed first for programming Lean itself, and second for build mathematical data structures and algorithms.
If you are not familiar with functional programming: you will be by then end of this book.
Here is an example program:
def remove_zeros (L : List ℕ) : List ℕ := match L with
| [] => List.nil
| x::Q => if x = 0 then remove_zeros Q else x::(remove_zeros Q)
#check remove_zeros
#eval remove_zeros [1,2,3,0,5,0,0]
Note the similarity between def
and theorem
. The latter is simply a special kind of definition.
Documentation
-
Loogle - Google for Lean
-
Lean Metaprogramming -- Advanced!
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
The Simply Typed λ-Lambda Calculus
Background
The λ-calculus was introduced in the 1930s by Alonzo Church as a way to represent how functions on natural numbers are calculated using symbols. The goal was to determine whether every function on the natural numbers had an effective means of being calculated.
Said differently, the question is: Does every function have an algorithm? Astonishingly, Church showed that the answer is "no". In fact, there are functions on the natural numbers for which there is no effective algorithm. Church's 1935 paper "An unsolvable problem in elementary number theory" proved this result.
The reasoning, roughly, is this:
- Devise a simple programming language, the λ-calculus
- Define computation as rewriting operations on λ-calculus terms
- Correspond to every term a natural number
- Conclude that questions about terms are thus questions about numbers
- Show there are more functions from terms into terms than there are terms.
A specific problem that Church showed to be unsolvable is: Given λ-calculus terms M and N, show there does not exist a λ-calculus function that can determine whether M can be rewritten as N. Those who have studied theoretical computer science, may be familiar with Alan Turing's similar result which shows there is no Turing Machine that can determine whether a given Turing Machine eventually terminates. In fact, the λ-calculus can simulate Turing Machines and vice verse.
The Church-Turing Thesis is the observation that all formalizations of computation are in fact equivalent to the λ-calculus or, equivalently, Turing Machines. The former is more convenient for symbolic reasoning, while the latter is more akin to how electromechanical computers actually work.
Programming Languages
Thus, the λ-calclus and the formal notion of computation has its roots in the foundations of mathematics. Later, around the 1960s, linguists and computer scientists realized that the λ-calculus was an useful framework for the theory and design of programming languages.
Simultaenously, logicians were becoming frustrated with Set Theory as a foundation for mathematics and started exploring Type Theory as an alternative. Around the 1990s many of these ideas came together, especially through the work of Thierry Coquand on the Calculus of Constructions. It was observed that typed programming languages were not only an ideal foundation for all of mathematics, they could be used to develop computational proof assistants and theoerm provers.
Curry's Paradox
The original formulation of the λ-calculus allowed for infinite loops, as do most programming languages. This made the λ-calculus expressive enough for Church to prove his undecidability results, but it caused other problems when logicians wished to use formalisms like the λ-calculus as systems of logic.
Haskel Curry discovered that one could encode the following paradox:
- Consider the self-referential statement X = X → Y where Y is any statement.
- Certainly X → X is true for any statement X.
- Substituting X → Y for the second X gives X → (X → Y)
- This statement is equivalent to X → Y, which is the same as X
- Thus X is true
- So Y is true since X → Y
For example, X → Y could mean "If this sentence is true, then 1 > 0." Any framework in which you can make this argument allows you to prove any statement Y, and so the framework is useless logically.
Types
The solution was to give types to all terms in the λ-calculus. We will see below that certain self referential programs are impossible to assign types to. At the same time, infinite loops are no longer allowed, making the formalism not as powerful from a computational point of view.
Thus was born the simply-typed λ-calculus. Eventually, more complicated types were added, in which type definitions could depend on other types or on even terms. Most modern programming languages and some logical frameworks have these properties.
Church's paper on the subject is quite complicated, elucidating ideas that were fairly novel at the time. Since then, comptuer scientists have refined the ideas into a very simple framework, which is presented here, and which can be found in numerous textbooks. The notes in the first part of this section follow video lectures by students of Prof. Uwe Nestmann at the Technical University of Berlin, except that I have restated the formulas in Lean. A link to the videos can be found in the references at the end of this chapter. A Google search will yield hundreds of similar lectures, notes, books, and papers.
Basic Types
The simply typed λ-calculus
is an extremely simple programming language that nevertheless captures the essence of computation. It uses type expressions and terms that have those types. We start with the types. First, we assume a base type. In Lean the base type is called Type
. You can ask lean what Type
is using the #check
directive (which stands for "Type Check").
#check Type
Lean tells you Type
has Type 1
, which is a synonym for Type
. Don't worry about this right now and just accept that Type
is a type. One constructs new types using the arrow →
as in the following examples:
#check Type → Type
#check Type → (Type → Type)
#check (Type → Type) → Type
That is, whenevever τ is a type, so is τ → τ. Arrow types are supposed to denote function types. So τ → τ is the type of any function that takes objects in τ and returns objects in τ. Note that the arrow → associates to the right. So the second expression above is equivalent to Type → Type → Type
.
Type Variables and Applications
You can also define type variables using def
def A := Type
def B := Type → Type
Which looks a bit more like what you would see in a textbook on type theory. Now you can construct more types.
#check A → B
Terms
Next, we define the terms of the lambda calculus. These are the programs. We start with variables, for example x
and f
, which we declare in Lean as follows:
variable (x : A) -- declare a variable x of type a
variable (f : A → A) -- declare a function f from A into A
#check x
#check f
What we've said here is that x
is a simple object with type A
, while f
is an function type from A
into A
. Next we have applications. These have the form e₁ e₁
where e₁
and e₂
are terms. For example,
#check f x
#check f (f x)
#check f (f (f x))
are all applications of terms to terms.
Abstractions
Finally, we have abstractions, which have the form λ (x : τ) => e
where τ
is a type and e
is a term. The x
in this expression is said to be bound
to the abstraction. It is a dummy variable and could be renamed without any change in meaning. For example, the following are terms in the λ-calculus:
#check λ (y : A) => y
#check λ (g : A → A) => λ (y : A) => g y
In the first example, the abstraction defines a function that simply returns its argument. In the second example, the abstraction defines a function that takes another function g
and returns yet another abstraction that takes an object y
and returns g
applied to y
.
Note that the parentheses group to the right, so the second example is equivalent to:
#check λ (g : A → A) => ( λ (y : A) => g y )
In Lean, we can also abbreviate a chained lamdba abstractions by writing:
#check λ (g : A → A) (y : A) => g y
Equivalence with def
A lambda abstraction is basically an unamed function. You could also give your functions names and use def
.
def inc₁ (x : Nat) : Nat := x + 1
def inc₂ := λ x => x + 1
#eval inc₁ 3
#eval inc₂ 3
#eval (λ x => x + 1) 3
Currying
Consider the lambda abstraction
variable (X : Type)
variable (a : X)
#check λ (g : X → X) => λ (x: X) => g x
If we apply the abstraction to particular function, then we get another function.
#reduce (λ (g : X → X) => λ (x: X) => g x) (λ x => x)
This way this new function is obtained is called Currying after Haskel Curry. The function can then be applied again:
#reduce (λ (g : X → X) => λ (x: X) => g x) (λ x => x) a
Type Derivation
All terms have types. These can be found using type theory's derivation rules:
VAR: Variables are declared either globally to have a given type (using Lean's variable command) or are bound in a λ-expression.
ABST: The type of an abstraction is always of the form A → B where A is the type of the argument and B is the type of the result.
APPL: If f : A → B and x : A, then the type of the application of f to x is B.
These derivation rules are applied automatically by Lean in the process of type checking using the #check directive. We can see the types Lean derives as follows.
def h₁ := λ (y : A) => y
def h₂ := λ (g : A → A) => λ (y : A) => g y
#check x
#check h₁
#check h₂
#check h₁ x
#check h₂ h₁ --> Example of currying
#check h₂ h₁ x
Note: Currying is named after the Logician Haskel Curry, who studied Electrical Engineering at MIT in the 1920s, although he eventually switched to Physics.
Type Errors
The typed lambda calculus disallows expressions that do not follow typing rules. For example, the following expression produces a type error
#check_failure λ (g : A) => λ (y : A) => g y
because g is not declared to be a function type and therefore cannot be applied to y.
Another example is
#check_failure λ (y : A) => q
about which Lean complains because q has not been declared in the present context.
Judgements and Contexts
When you hover over a #check directive, Lean shows the results of the type derivation as what is called a judgement. It is an expression in two parts separated by a turnstile ⊢. For example: #check h₁ x
produces
x : A
f : A → A
⊢ A
Before the turnstile is the context, a list of all the variables introduced so far. After the turnstile is the type of (h₁ x), which in this case is A. In the literature, this written:
{ x : A, f : A → A } ⊢ h₁ x : A
which reads: "If A has type A and f has type A → A, then we can derive h₁ x has type A". In an expression such as
λ (y : A) => f y
the variable f is not bound to an enclosing lambda. In this case it is called free. The variable y on the other hand is bound
. Free variables have to be declared in Lean for expressions to use them. And they have to have types consistent to how they are used. When this is done properly, you will see the free variable declarations in the context part of Lean's results.
Beta Reduction
An abstraction can be applied
to another term to produce a new term. This is called β-reduction. It is defined like this:
(λ (x:α) => M) N —β→ M[x:=N]
The notation M[x:=N]
means: take all free occurances of x
in M
and replace them with the expression N. We have to be careful that N
does not use the variable x
freely. Lean does this internally for us The bound version of x
above is, internally, a completely unique variable that is just displayed as x
for our convenience.
To apply β-reduction in Lean, you can use the #reduce directive. For example, we can see that
(λ (g : α → α) => λ (y : α) => g y) f —β→ λ (y : α) => f y
This is obtained by replacing g
in g y
with f
, as the rule describes. You can have Lean do this for you using the #reduce directive. The #reduce
directive needs permission to be aggressive, which we can do using the (types := true)
option.
#reduce (types:=true) (λ (y : A) => y) x
#reduce (types:=true) (λ (g : A → A) => λ (y : A) => g y) (λ (y : A) => y)
#reduce (types:=true) (λ (g : A → A) => λ (y : A) => g y) (λ (y : A) => y) x
Properties of the Simply Typed λ-calculus
Some interesting observations are in order. We won't prove these here, but they are good to know:
Uniqueness of Types: Every term has exacly one type.
Subject Reduction Lemma: If M₁ : α and M₁ —β→ M₂
then M₂ : α
. That is, beta reduction does't change the type of expressions. It just simplifies them.
Church-Rosser Theorem: If M —β→ N₁
and M —β→ N₂
then there is some N₃
such that N₁ —β→ N₃
and N₂ —β→ N₃
. That is, it doesn't matter what order you β-reduce an expression's sub-expressions in, you always end up with the same term.
Strong Normalization: β-reduction eventually stops at an irreducible term. This is a very strong statement. In most programming languages, you can write infinite loops. But not in the simply typed λ-calculus!
Extended Example: Church Numerals
Even though the simply typed λ-calculus looks simple, you can encode quite a bit of math with it. The goal of this next section is to show you how do do arithmetic with only what we have so far (simple arrow types and terms depending only on terms). We do this not because it is efficient -- it isn't! Instead, we want to show that the essence of arithmetic is captured by the simply typed λ-calculus.
First, we need a way to represent numbers. Church devised the following scheme, where c₀ is the Church Numeral for 0 and so on.
def α := Type
def c₀ := λ ( f : α → α ) => λ ( x : α ) => x
def c₁ := λ ( f : α → α ) => λ ( x : α ) => f x
def c₂ := λ ( f : α → α ) => λ ( x : α ) => f (f x)
def c₃ := λ ( f : α → α ) => λ ( x : α ) => f (f (f x))
You can check the type of a Church numeral:
#check c₂
For convenience, let's give this type a name:
def N := (α → α) → α → α
#check N
Arithmetic
We can define functions on numbers. For example, the successor function is defined below.
def succ := λ (m : N) => λ (f : α → α) => λ (x: α) => f (m f x)
To see how this works, let's apply succ to c₀. We omit the types to make it easier to read. Note for clarity we use the dummy variables g and y in c₀ instead of f and x.
succ c₀ = ( λ m => λ f => λ x => f (m f x) ) ( λ g => λ y => y ) —β—> λ f => λ x => f ( ( λ g => λ y => y ) f x ) [note, g is not used, so f x disappears] —β—> λ f => λ x => f ( ( λ y => y ) x ) —β—> λ f => λ x => f x = c₁
This is a lot of work, so let's let Lean do this for us:
#reduce (types := true ) succ c₀
#reduce (types := true ) succ c₃
Other Operations
We can also add two numbers together:
def add := λ (m : N) => λ (n : N) => λ (f : α → α) => λ (x: α) => m f (n f x)
#reduce (types := true) add c₃ c₂
#reduce (types := true) add (succ c₃) (add c₁ c₂)
And here is multiplication:
def mul := λ (m : N) => λ (n : N) => λ (f : α → α) => λ (x: α) => m (n f) x
#reduce (types := true) mul c₃ c₂
We can even do a sort of if-statement:
def ifzero := λ (m : N) => λ (n : N) => λ (p : N) =>
λ (f : α → α) => λ (x: α) =>
n (λ ( y : _ ) => p f x) (m f x)
#reduce (types := true) ifzero c₂ c₀ c₃
#reduce (types := true) ifzero c₂ c₁ c₃
LEAN CAN PROVE 1+1 = 2
theorem one_plus_one_is_two : add c₁ c₁ = c₂ :=
rfl
You can prove this by rfl because, as we will see, two lambda expressions that beta reduce to the same thing are considered definitionally equal
. Although this is not scalable and in fact Lean has a much more expressive type system that we will harness soon.
Church Numerals are Inconvenient
You can define other opertations on the natural numbers in a similar fashion. It is also fairly straightforward to define Booleans and Boolean Logic, as well as a number of other basic mathematical structures.
Building up from these basic ideas to more complex mathematics is the point of Lean. Eventually, we will arrive at cutting edge mathematics in Lean. Because it is defined in terms of thee basic building blocks, we always have a proof that goes from the high level mathematica statements to the low level meaning in terms of the typed λ-calculus: That is, a proof from first princples.
That said, it will ultimately be better to define a richer set of types. For example, we'll define the natural numbers and almost every other mathematical object in Lean using what are called Inductive Types.
Type Theory Questions
Now that we have a simple programming language and a way to assign types to terms in that language, we can explore a number of problems in type theory, each with its own purpose and challenges.
TYPE CHECKING: In a given context, does a term M have a given type σ?
Γ ⊢ M : σ
WELL TYPEDNESS: Does there exist a context in which a type be assigned to a term M? Another way of saying this is "is M a legal term?"
? ⊢ M : ?
TYPE INFERENCE: Can M be assigned a type consistent with a given context?
Γ ⊢ M : ?
INHABITATION: Does there exist a term of a given type? If σ is a logical statement, then this is the question of whether σ has a proof.
Γ ⊢ ? : σ
Type Inference
Lean is good at type inference. We can go a step further with Lean and leave out types in expressions, letting Lean infer what they must be. For example, the Church numerals can be written more consicely, skipping some of the type declarations and using multi-argument lambdas, as follows:
#check λ _ y => y
#check λ ( g : α → α ) y => g y
#check λ ( g : α → α ) y => g (g y)
We haven't said what the type of y is in these expressions. And we haven't even given the first bound variable in c₀ a name, since it isn't used in the the body of the abstraction. Lean infers that y must have type α because it is being acted upon by a function from α to α. We can also write the other operations, like multiplication, more concisely:
#check λ (m n : N) f x => m (n f) x
We can't leave out all of the type information though. Consider:
#check_failure λ g y => g y
In the above, there are any number of ways types could be assigned to g and y, so Lean complains that it can't assign types to them. So while the expression is typeable, Lean can't infer a type for it and you have to give it more information.
Self-application is Untypeable
Dropping types for the moment, define the term
Ω := λ x => x x
and consider Ω
applied to itself Ω
:
(λ x => x x) (λ x => x x) —β—> (λ x => x x) (λ x => x x)
producing an infinite loop. Suppose you could give M M
a type:
M M : σ
For this to work, M
has to be a function:
M : τ → σ
But since M
is operating on itself, M
has to be of type τ
:
M : τ
So M
has two different types, which is not possible. Lean is not able to find a type for x
. The placeholder symbol _
is used by Lean as a way to ask the type checker to infer a type.
#check_failure (λ (M:_) => M M)
Propositions
Lean has a special type called Prop
which stands for Proposition
. It treats this type somewhat differently than all other types, but in most ways it ist just another type.
variable (p : Prop)
#check Prop
#check p
If p is of type Prop
, then an element hp : p
is evidence that the type p is not empty. Alternatively, you can think of hp as a proof
of p.
Furthermore, arrow types which above denoted functions, can be thought of as denoting implication if Prop
is involved.
#check p → p
Armed with the lambda calculus and we can now prove theorems involving implication:
example (p : Prop) : p → p :=
λ hp => hp
example (p q : Prop) : p → (p → q) → q :=
λ hp => λ hpq => hpq hp
Why is it Called "Simply Typed"?
You might be asking yourself, is there a non-simply typed λ-calculus? The answer is yes! We will get there eventually. Here's a preview:
Simple types: Terms depend on other tems. This is what we've covered so far. For example, the body of a lambda abstraction (a term) depends on the bound variable (also a term).
#check (λ x : Nat => x+1) --- the term x+1 depends on the term x.
Polymorphism: Terms can depend on types. Polymorphism allows us to write functions that operate on a variety of types, instead of just a single type, by taking the type to be operated on as an argument. For example, the identity function λ x : A => x
only operates on elements of type x. What if we wanted to define an arbitrary identity function for any type. Here is one way:
#check (λ α : Type => λ x : α => x) -- a polymorphic identity function.
A better way would be:
universe u
def my_id {α : Type u} (x : α) := x
#check my_id 1
#check my_id "one"
#check my_id my_id
Note the curly braces around α : Type u
specify that the argument α
is implicit. That is, that Lean should try to infer what it is. In the the examples #check
statements above, Lean figures out which type the argument is, and therefor which type the overall expression is, by inspection.
Parameterized Types: Types can depend on types. The idea here is to build a type from other types. For example, a List type is fine, but it would nice to avoid having two make a separate data type for lists of different types of objects. In fact, Lean's standard library defines List
as a parameterized type. You can see in the first #check
below that making a list requires a type as an argument
#check List -- Requires a type as an argument
#check List Nat -- The type of a list of natural numbers
#check List (List Nat) -- The type of a a list of list of natural numbers
Lean is also good at figuring out what kind of list you are talking about in most contexts, as the following examples show.
#check [1,2,3]
#check ["one","two","three"]
Dependent types: Types can depend on terms. Finally, we can have types that depend on terms. For example, the type of vectors (from Lean's standard library) of natural numbers of length 10 depends on the term 10.
#check Vector Nat 10 -- Vectors of 10 Nats
Calculus of Constructions: If we allow all of the above in type theory, we get what is called the Calculus of Constructions, or CoC. This theory was first described by Thierry Coqrand and emboded in the Coq proof assistant, now called Rocq. Lean and other proof assistants are also based on CoC.
Inductive Types: Types can be defined inductively. For example, the natural numbers are defined by a base case (zero) and a successor function (succ), from which all other natural numbers can be constructed. This is discussed in more detail in the chapter on Inductive Types.
Quotients: Quotients of types via equivalence relations. For example, a real number is defined to be the set of all Cauchy sequences of rational numbers that converge to it. That is, the reals are the quotient of the set of Cauchy Sequences by Cauchy equivalence. This is discussed in more detail in the chapter on Quotients.
Looking ahead: the Curry-Howard Correspondence
The most important problem in using type theory for proofs is INHABITATION, followed by TYPE CHECKING. To motivate why, we will see later the following remarkable fact, called the Curry-Howard corresponence, which says that in the judgement Γ ⊢ M : σ,
- Γ can be considered a set of givens or assumptions
- σ can be considered a mathematical statement like a theorem or lemma
- M can be considered a proof of the theorem assuming the statements in Γ.
Thus, type checking amounts to checking that M is a proof of σ, which is a relatively straightfoward problem and we have seen that Lean is quite good at it. This is why tools like Lean are called proof assistants
. They check to make sure your proofs are correct.
On the other hand, type inhabitation amounts to finding a proof of σ. This is a very difficult problem, essentially the job of the working mathematician. From a computational point of view, finding a proof means searching over terms M until one finds one that has type σ. Depending on how expressive the programming language for terms is, this can either be a computationally intractable problem (meaning search is the best you can do) or it can be a computationally unsolvable problem (meaning there may not be an algorithm that is guaranteed to find an M of type σ). Both of these observations are job security for mathematicians!
Going a step further, we'll see that an abstraction
λ p : σ => q
which may have type
σ → τ
is the general form of a proof of the statement σ → τ where → means "implies". It can be thought of as a transformation taking a proof of σ, which one assumes is available, and returning a proof of τ, which is thought of as a goal to be proved. Writing the details of what q is amounts to programming.
As a computer scientist myself it is very satisfying to know that programming functions with given type specifications is the same thing as proving theorems!
This idea is not merely cute. By building on it, as Lean and similar tools do, one can enocde an astonishingly large set of all of mathematics, and presumably knowledge in general. We'll learn how to take advantage of the Curry-Howard corresponence soon.
References
Alonzo Church An Unsolvable Problem of Elementary Number Theory. American Journal of Mathematics, 1936.
Haskell B Curry The Inconsistency of Certain Formal Logics. The Journal of Symbolic Logic, 1942.
Alonzo Church A formulation of the simple theory of types. Journal of Symbolic Logic, 1940
Uwe Nestmann and Students The Lambda Cube Unboxed. YouTube, 2020
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Propositional Logic
Propositions
A proposition is a statement that is either true or false. The following are examples:
- It is raining outside.
- All cats are animals.
- Darth Vader is Luke's Father.
- Four is greater than five.
In propositional logic, we assign propositional variables to represent the value of these statments. So we might make the assignments:
- p := It is raining outside.
- q := All cats are animals.
- r := Darth Vader is Luke's Father.
- s := Four is greater than five.
In Lean, we declare propositional variables as follows:
variable (p q r s : Prop)
Note that we are not saying p is the English language sentence "It is raining outside". We are not doing natural language processing here. Rather, we are saying that p
is a propositional variable that is true when it actually is raining outside, and false otherwise. To determine the truth value of p
, we would need some way to check whether it is raining outside (as well as some specifics like outside where and when? For now, we'll just be informal about such things).
Atomic vs Compound Propositions
A propsition that corresponds to a direct measurement or other basic truth is called atomic. It cannot be sub-divided into more basic propositions. Otherwise it is called compound. For example, the proposition
- It is raining outside or all cats are animals.
is a compound proposition that uses the connective "or", written as ∨
to connect two atomic propositions. Symbolically, we write
#check p ∨ q
to check that the compound p ∨ q
is a proposition.
Students used to digital logic will wonder why we are using ∨ instead of the symbol +. The main reason is that + will usually mean actual addition when things get more complicated. Thus, mathematicans have invented new symbols for logical connectives. Here are the most important for our current purposes:
#check ¬p -- not p
#check p ∨ q -- p or q
#check p ∧ q -- p and q
#check p → q -- p implies q
#check p ↔ q -- p if and only if q
#check True
#check False
We also have the propositional False
which denotes absurdity. In intuitionistic logic, ¬p
is just shorthand for
p → False
#check False
#check p → False
Also note that ↔ is just shorthand for → in both directions
p ↔ q ≡ p → q ∧ q → p
Constructive Proofs
The goal is this chapter is to define a mathematical framework in which we prove statements by constructing proofs. In particular,
- To prove p ∧ q we construct a proof of p and another proof of q.
- To prove p ∨ q we construct a proof of p or we construct a proof of q.
- To prove p → q we supply a method for converting a proof of p into a proof of q
- To prove ¬p (which is p → ⊥) we supply a method to convert a proof of p to ⊥
Example: A Constructive Proof in Lean
example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) :=
Iff.intro
(λ h : p ∧ (q ∨ r) =>
have hp : p := h.left
have hqr : q ∨ r := h.right
Or.elim hqr
(λ hq : q => Or.inl (And.intro hp hq))
(λ hr : r => Or.inr (And.intro hp hr))
)
(λ h : (p ∧ q) ∨ (p ∧ r) =>
Or.elim h
(λ hpq : p ∧ q => And.intro hpq.left (Or.inl hpq.right))
(λ hpr : p ∧ r => And.intro hpr.left (Or.inr hpr.right))
)
Don't worry if this doesn't make sense right now. It will soon.
Comparison to Classical Logic
We have defined intuitionistic logic or constructive logic, different from classical logic. In classical logic, the truth of a statement like
p ∨ ¬p
is guaranteed by the law of the exluded middle. You know one of them must be true. In constructive mathematics, you have to either construct a proof of p
or construct a proof of ¬p
. As an example, consider the proposition:
The universe is infinite or the universe is finite.
Neither part of this compound proposition currently has a proof. Classically, we would still conclude it is true, but constructively we are just stuck. Similar issues arise with famous mathematical conjectures such as
P = NP or P ≠ NP
and
There are either a finite number of twin primes, or an infinite number of twin primes.
These statements may be proved some day, but for now, we cannot conclude they are true using constructive mathematics.
Double Negation
Similar to the law of the excluded middle is double negation:
¬¬p ↔ p
Classically, this is a tautology (a proposition that is always true). But constructively, from a proof of "it is not the case that p is not true" one cannot necessarily construct a proof that p
is true.
As a result, proof by contradiction
is not valid constructively, because in proof by contradition one follows the procedure:
To prove `p`, assume `¬p` and derive `False`.
Just because we have a way to transform a proof of ¬p
into False
does not mean we can have a construction of a proof of p
.
Classical Logic
TODO
Contexts
We now begin to build a framework for proving theorems in propositional logic. The first thing we need is a way to keep track of what propositions we currently know in the course of proving something.
To this end we define a context to be a finite set of propositions. Given two contexts Γ
and Δ
we can take their union Γ ∪ Δ
to make a new context. The notation is a bit cumbersome, so we write Γ,Δ
instead. In particular, if φ ∈ Φ
then Γ,φ
is shorthand for Γ ∪ {φ}
.
If we can show that a proposition φ
is true whenever all the propositions in a context Γ
are true, we write
Γ ⊢ φ
which reads gamma entails
φ
. Furthermore, if a proposition φ
is tautology (meaning it is always true like p ↔ p
) then it is true independent of any context. That is, the empty context entials any tautology. Thus, we write
⊢ φ
to mean ∅ ⊢ φ
. We will define precisely what the entails relationship means next.
Rules of Inference
A rule of inference is set of premises and a conclusion that can be drawn from those premises. The propositional logic we define has only a handful of rules of inference from which all proofs can be constructed. They are presented with a name followed by what looks like a fraction with the premises listed on top and the conslusion on the bottom.
Γ₁ ⊢ A Γ₂ ⊢ B Γ₃ ⊢ C
RULE NAME ————————————————————————————
Γ ⊢ D
In this schemantic, the rule has three premises, each of which describe an assumption that a particular context entails a particular proposition. And the rule has one conclusion, stating the entailment you are allowed to conclude. Usually, the contexts listed and the propositions are related in some way. - #
Axioms
The first rule has no premises and simply states that φ
can be concluded from any context containing φ. Said constructively, if we have a proof of φ
, then obviously we can construct a proof of φ
.
AX ——————————
Γ,φ ⊢ φ
Here is a simple proof that {hp:p} ⊢ p
in Lean using the Axiom rule:
example (hp : p) : p :=
hp
If you look at this proof in the infoview, putting your cursor at the beginning of the second like, you will see
hp : p
⊢ p
Which says, give we have a proof hp
of p
, we need show p
. This is easy, we jsut use hp
itself.
Implies Rules
We have two rules for the → connective:
Γ,φ ⊢ ψ
→-Intro ————————————
Γ ⊢ φ → ψ
Γ ⊢ φ → ψ Γ ⊢ φ
→-Elim —————————————————————
Γ ⊢ ψ
The Implies Introduction rule allows us to introduce φ → ψ
whenever we have Γ
and φ
together entailing ψ
. The Implies Elimination rule is also know as Modus Ponens. It states that if we know φ
implies ψ
and we know φ
, then we know ψ
.
Notice that implies is written with an arrow →
just like function abstraction in the λ-calculus. This is because one way to think about a proof of φ→ψ
is to require it to have the form of a function that converts proofs of φ
into proofs of ψ
. This suggests that the way to prove statements with implications is to use λ-calculus expressions. Here are a couple of examples.
First we show and example of →-Intro. The context includes a proof of p
. Thus we can introduce q→p
for any q
. We do this with a lambda expression taking a proof of q
(and in this case ignoring it) and returning the proof hp
of p
available in the context.
example {hp : p} : q → p :=
λ hq => hp
Next, here is an example of →-elim. We have a context with a proof of p→q
and a proof of p
. We know the proof hp
of p→q
is a lambda abstraction. So we can apply it to a proof hp
of p
to get a proof of q
.
example {hpq: p → q} {hp: p} :=
hpq hp
A complete description of how →-introduction works works is in the chapter on the Curry-Howard Isomorphism
And Rules
Next we have three rules for the ∧ connective:
Γ ⊢ φ Γ ⊢ ψ
∧-Intro ———————————————————
Γ ⊢ φ ∧ ψ
Γ ⊢ φ ∧ ψ
∧-Elim-Left ——————————————
Γ ⊢ φ
Γ ⊢ φ ∧ ψ
∧-Elim-Right —————————————
Γ ⊢ ψ
Whenever we see "Intro" that means we are introducing a connective (in this case ∧
) into our conclusion. Whenever we see "Elim" that means we are eliminating part of a compound statement in our conclusion. Here, the And Introduction rule shows that we can construct a proof of φ ∧ ψ
whenever the context contains a proof of φ
and a proof of ψ
. The And Elimination rules allow us to "eliminate" half of the proposition φ ∧ ψ
to conclude the weaker statement φ
or to conclude the weaker statement ψ
. Said differently, if we have a proof of φ∧ψ
then we can construct a proof of φ
by just eliminating the part of the proof of φ∧ψ
that shows ψ
.
Unlike the somewhat cryptic rules for implies, the And rules just have functions (like And.intro
) already defined for them. Here are examples of all of these rules in Lean.
#check And.intro
#check And.left
#check And.right
example (hp : p) (hq : q) : p ∧ q :=
And.intro hp hq
example (hpq : p ∧ q) : p :=
And.left hpq
example (hpq : p ∧ q) : q :=
And.right hpq
Or Rules
Then we have three rules for the ∨ connective:
Γ ⊢ φ
∨-Intro-Left ———————————
Γ ⊢ φ ∨ ψ
Γ ⊢ ψ
∨-Intro-Right ————————————
Γ ⊢ φ ∨ ψ
Γ ⊢ φ ∨ ψ Γ ⊢ φ → ρ Γ ⊢ ψ → ρ
∨-Elim ———————————————————————————————————————
Γ ⊢ ρ
The Or Introduction rules allow us to conclude φ ∨ ψ
from one of its parts. The Or Elimination rule looks complicated, but it is straightforward. It says that if we know Γ ⊢ φ ∨ ψ
then we know that Γ
must entail either φ
or ψ
. If we also know that both φ
and ψ
separately entail ρ
, then we know that Γ
must entail ρ
.
Here are examples of the OR rules in Lean.
#check Or.inl
#check Or.inr
#check Or.elim
example (hp : p) : p ∨ q :=
Or.inl hp
example (hq : q) : p ∨ q :=
Or.inr hq
example (hpq : p ∨ q) : q ∨ p :=
Or.elim
hpq
(λ hp => Or.inr hp)
(λ hq => Or.inl hq)
Ex Falso
Finally, we have the a rule for the ¬ connective:
Γ ⊢ False
False -Elim ————————————
Γ ⊢ φ
which states that you can conclude anything if you have a proof of ⊥. This rule is also know as ex falso sequitur quodlibet
or just ex falso
or the principle of explosion
! Here's an example:
#check False.elim
example { hf : False } : p :=
False.elim hf
Proofs
A proof that Γ ⊢ φ
is sequence of statements of the form Γ' ⊢ φ'
each of which is either (a) an axiom or (b) obtained from previous statements in the sequence by one of the inference rules.
Example 1
As an example, we will prove the statement
∅ ⊢ (p∧q)→p
Working backwards from this goal, we see that →-Intro
can be applied to produce this statement where φ
is p∧q
and ψ
is p
. Thus, we get an instance of →-Intro of the form
p∧q ⊢ p
——————————— (Instantiation of →-Intro)
⊢ (p∧q)→p
We have now a simpler problem, which is to show p∧q ⊢ p
. The ∧-Elim-Left rule applies here with φ=p∧q
, ψ=p
, and Γ={p∧q}
giving us the instance
p∧q ⊢ p∧q
—————————————— (Instantiation of ∧-Elim-Left)
p∧q ⊢ p
And now we have an even simpler problem, which is to show that p∧q ⊢ p∧q. But this matches the axiom rule with Γ={p∧q}
and φ = p∧q
. Putting all this together into a proof gives us the following:
1) p∧q ⊢ p∧q axiomatically
2) p∧q ⊢ p from (1) via ∧-Elim-Left
3) ⊢ (p∧q)→p from (2) via →-Intro
And that's it. Our first proof!
Here is the same proof in Lean:
example : p∧q → p :=
λ hpq => And.left hpq
The lambda expression encodes →-Intro, and And.left
encodes ∧-Left.
What you can take away from this is the idea that constructing this proof is a purely syntactic endeavor. One can easily imagine an algorithm that does this automatically by pattern matching a given sub-goal against the Γ
, φ
and ψ
in the description of a inference rule. The challenge is, of course, as we introduce more expressibility into our logic, and more inference rules, finding the right rules to apply at the right time amounts to a brute force search of all possible inference rules and all possible ways of instantiating those inference rools.
The other thing to notice is that the proof itself looks a lot like a program. In Lean and similar construction-based theorem provers, this observation is made precise. And it will turn out that writing proofs and writing programs amount to the same thing!
Example 2
Here is a slightly more complicated example. Let's prove
⊢ ¬p→(p→q)
Recall ¬p
is just shorthand for p→⊥
. So we're actually trying to prove
⊢ (p→⊥)→(p→q)
Once again, working backwards, we can apply →-Intro to get
p→⊥ ⊢ p→q
and then apply →-Intro again to get
p→⊥,p ⊢ q
Our context now contains both ¬p
and p
. Using ⊥-elim we get
p→⊥,p ⊢ ⊥
This subgoal matches the form of →-Elim with φ=p
and ψ=⊥
. Using this rule, we get two further subgoals that are just axioms:
p→⊥,p ⊢ p→⊥ and p→⊥,p ⊢ p
Putting this all together, we get the following proof:
1) p→⊥,p ⊢ p→⊥ axiomatically
2) p→⊥,p ⊢ p axiomatically
3) p→⊥,p ⊢ ⊥ from (1) and (2) via →-Elim
4) p→⊥,p ⊢ q from (3) via ⊥-elim
5) p→⊥ ⊢ p→q from (4) via →-Intro
6) ⊢ (p→⊥)→(p→q) from (5) via →-Intro
And we're done! This is complicated though. Clearly we need a proof assistant to help us! In Lean this proof looks like:
theorem t : ¬p→(p→q) :=
λ hnp => λ hp => False.elim (hnp hp)
The Law of the Excluded Middle
As we said, the law of the excluded middle states that
⊢ φ ∨ ¬φ
for all propositions φ. However, this statement is not provable using the inference rules above. To prove this meta-mathematical observation is beyond the scope of this lecture and requires an argument about the formal semantics
of intuitionist propositional logic. For now, accept the fact that φ ∨ ¬φ cannot be proved rom the inference rules given, despite its seeming obviousness.
For this reason, intutionistic logic is weaker than classical logic. However, we can obtain classical logic by adding the above as a new axiom. When we get to proving statements in Lean, we'll see that we can add this axiom into our proofs if we would like to, so it is not big problem. However, it is also remarkable how much of mathematics we can do without this axiom.
Exercises
- Prove
∅ ⊢ p → (p ∨ q)
- Prove
∅ ⊢ (p ∨ q) → (q ∨ p)
REFERENCES
Morten Heine Sørensen, Pawel Urzyczyn "Lectures on the Curry-Howard Isomorphism" Elsevier. 1st Edition, Volume 149 - July 4, 2006.
- Chapter 2 describes Intuitionistic Propositional Logic
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
The Curry-Howard Isomorphism
Much of this chapter is an adaptation of the section of the book Lectures on the Curry-Howard Isomorphism by Morten Heine Sørensen and Pawel Urzyczyn. In particular, Chapter 4 of that book describes Intuitionistic Propositional Logic.
Statements, Contexts, and Judgements
When we introduced the Simply Typed Lambda Calculus, we informally defined the type rules VAR, ABST and APPL. Here we define the typing system formally.
-
A type statement is a pair x : σ where x is a type variable and σ is a type. We say "x is of type σ".
-
A typing context Γ is a finite set of type state statements.
-
A judgement is an expression of the form Γ ⊢ M : σ where Γ is a typing context, M is a simply typed λ-calculus statement, and σ is a type.
For example, here is a judgment that states: "When f is a function from α to β and x is of type α, then f x is of type β. "
{ f : α → β, x : α } ⊢ f x : β
Typing Rules
Typing rules are written the same way as the inference rules in propositional logic.
VAR ————————————————
Γ,x:τ ⊢ x:τ
Γ,x:σ ⊢ M : τ
ABST ——————————————————————————
Γ ⊢ (λ x:σ => M) : σ→τ
Γ ⊢ M : σ→τ Γ ⊢ N : σ
APPL ——————————————————————————————
M N : τ
The first rule says that if a context defines x to have type τ then (somewhat obviously) we can conclude x has type τ.
The second rule says that if our context defines x : σ and entails that M : τ, then we can form an abstraction from x and M that has type σ to τ.
The third rule says that if Γ entails both that M : σ→τ and N : σ, then the application of M to N has type τ.
Example
Q: Find the type of
λ x : A => x
A: Working backwards from this goal we use ABST with τ=A and M=x to get
x:A ⊢ x:A
Then we use VAR. So the expression has type A→A and a proof of this is:
1) x:A ⊢ x:A axiomatically
2) (λ x : A => x) : A→A by ABST
As we have seen, Lean figures this out automatically.
#check λ x : _ => x
EXAMPLE
Q: Find the types of x and y in
λ x => λ y => x y
A: Using the ABST rule gives
x : B ⊢ λ y => x y : A
for some types A and B. Using ABST again gives
x : B, y : C ⊢ x y : A
for some type C. Next we use the APPL rule with M = x, N = y, σ = C, τ = A
x : B, y : C ⊢ x : C → A
x : B, y : C ⊢ y : C
These judgements would hold if B we equal to C→A. So we make that substitution so the above axioms hold to get:
λ x : C → A => λ y : C => x y
for some types C and A. Generally speaking, type inference involves applying typing rules, accumulating type equations, and then solving the equations, all of which is done very efficiently in Lean's kernel.
Example
Q: Find the overall type of the previous expression.
A: Following the derivation above in reverse gives the following type inference proof tree:
————————————————————————————— VAR ————————————————————————————— VAR
x : C → A, y : C ⊢ x : C → A x : C → A, y : C ⊢ y : C
———————————————————————————————————————————————————————————————————— APPL
x : C → A, y : C ⊢ x y : A
————————————————————————————————————————— ABST
x : C → A ⊢ λ y : C => x y : C → A
————————————————————————————————————————————————————— ABST
⊢ λ x : C → A => λ y : C => x y : (C → A) → C → A
Thus, the type of λ x => λ y => x y
is (C → A) → C → A
. Note that with a little help, Lean can figure this out for us, but we do need to tell it that x
is a function type of some kind.
#check λ x : _ → _ => λ y : _ => x y
Curry-Howard Isomorphism Intuition
Consider the two types we just found:
A → A
(C → A) → C → A
The first one is the type of a function on. The second one is the type of a function that takes a function on C → A
.
Wwe can also read these as propositional formulas which state
A implies A
(C implies A) implies C implies A
It is not a coincidence that these are both tautologies.
The Curry-Howard Isomorphism emerges from the observation that the λ expressions that have the above types look a lot like the proofs that the above implications are tautologies!
With this observation, the statement x : A reads "x is a proof of A".
λ x : A => x
is a method that takes a proof of A and returns a proof of A, proving the implication A → A.
Curry-Howard: Types → Propositions
To state the CHI exacly, we will restrict ourselves to showing that Propositional Logic with only implication (→) is isomorphic to the simply typed λ-calculus. We will need one definition.
Def: Given a context Γ = { x₁: φ₁, x₂ : φ₂, ..., xₙ : φₙ }, the range of Γ, denoted |Γ|, is { φ₁, φ₂, ..., φₙ }.
Theorem: If Γ ⊢ M : φ then |Γ| ⊢ φ.
Proof Sketch: We convert any type derivation tree into a propositional proof by replacing VAR with AX, ABST with →-Intro, and APPL with →-Elim. This is done by induction on the proof tree. Here we just show an example which should be easily generalized. The type proof tree in the previous section can be re-written be removing all "x : "
————————————————————— AX ———————————————————— AX
C → A, C ⊢ C → A C → A, C ⊢ C
——————————————————————————————————————————————————————————— →Elim
C → A, C ⊢ A
——————————————————— →-Intro
C → A ⊢ C → A
—————————————————————— →-Intro
⊢ (C → A) → C → A
Curry-Howard: Propositions → Types
The opposite direction of the CHI is more technical. We have to show how to produce a λ-calculus term M from a proof of φ
so that M : φ
. For example, suppose we started with the propositional proof tree in the previous section. How would we produce the type derivation from it? Here we will outline how this is done in general.
First we need a way to produce a type context from a propositional context. Suppose that
Γ = { φ₁, φ₂, ..., φₙ }
and define
Δ = { x₁ : φ₁, x₂ : φ₂, ..., xₙ : φₙ }
where the xᵢ
are introduced as new type variables. The object Δ
is a function of Γ
of course, but we just don't write it this way.
Theorem: If Γ ⊢ φ
then there exists a λ-calculus term M
such that ∆ ⊢ M:φ
.
The proof of this theorem uses induction on the proof tree that shows Γ ⊢ φ
. Since there are three rules (AX, →Intro, and →-Elim), we have three cases, which we handle one by one.
Case: The proof ends with Γ,φ ⊢ φ
by the VAR rule
Subcase 1: If φ ∈ Γ
then there is some type variable x
such that x : φ ∈ Δ
. By the VAR rule we can conclude
Δ ⊢ x : φ
Subcase 2: If φ ∉ Γ
then we introduce a new variable x
such that x : φ
. Once again by the VAR rule
Δ, x : φ ⊢ x : φ
(Why do we need two sub-cases? It's because of how we defined Δ
on the previous as related to Γ
and not to Γ ∪ { x : φ }
).
Case: The proof ends with →Elim
Suppose the proof that Γ ⊢ φ
ends with
Γ ⊢ ρ → φ Γ ⊢ ρ
——————————————————————————
Γ ⊢ φ
We need to find a λ-term that has type φ
. Here the premises of the above rule instance allow us to assume the induction hypothesis that there exists M
and N
such that
Δ ⊢ M : ρ → φ
Δ ⊢ N : ρ
By the ABST rule, we can conclude
Δ ⊢ M N : φ
Case:: The proof ends with →Intro
Suppose the proposition φ
has the form the ρ → ψ
and the proof Γ ⊢ ρ → ψ
ends with
Γ, ρ ⊢ ψ
——————————————
Γ ⊢ ρ → ψ
Subcase 1: ψ ∈ Γ
. By the induction hypothesis, there is a term M
such that Δ ⊢ M : ψ
. Introduce a variable x
(not used in Δ
) such that x : ρ
. Then we can conclude
Δ, x : ρ ⊢ M : ψ
and by the ABST rule
Δ ⊢ λ x : ρ => M : ρ → ψ
Subcase 2: ψ ∉ Γ
. Then by the induction hypothesis, there is a term M
such that
Δ, x : ρ ⊢ M : ψ
from which we may also conclude
Δ ⊢ λ x : ρ => M : ρ → ψ
Propositions, Theorems, and Proofs in Lean
The Curry-Howard approach is exactly how proofs of theorems are done in Lean. We show that the proposition to be proved is inhabited. In the examples below, we use the type Prop, from Lean's standard library.
We will start by declaring two variables of type Prop. We use curly braces here instead of parentheses for reasons we will explain later.
variable { A C : Prop }
To prove a proposition like A → A, we define the identity function from A into A, showing the proposition considered as a type is occupied. We have called the bound variable in the lambda expression proof, but you could call the bound variable anything you like.
def my_theorem : A → A :=
λ proof : A => proof
Lean provides the keyword theorem for definitions intended to be results, which is like def but does requires the type of the theorem being defined to be Prop. The theorem keyword also gives Lean and the user an indication of the intended use of the definition.
theorem my_lean_theorem : A → A :=
λ proof : A => proof
APPLYING THEOREMS TO PROVE OTHER THEOREMS
As another example, we prove the other proposition we encountered above. Here we call the bound variables pca for "proof of c → a" and pc for "proof of c".
theorem another_theorem : (C → A) → C → A :=
λ pca : C → A =>
λ pc : C =>
pca pc
Or even better, we can use our first theorem to prove the second theorem:
theorem another_theorem_v2 : (C → A) → C → A :=
λ h : C → A => my_lean_theorem h
More Examples
theorem t1 : A → C → A :=
λ pa : A =>
λ pc : C => -- Notice that pc is not used
pa
theorem t2 : A → C → A :=
λ pa pc => pa -- We can use λ with two arguments
theorem t3 : A → C → A :=
λ pa _ => pa -- We can tell Lean we know pc is not used
example : A → C → A := -- We can state and prove an unnamed theorem
λ pa _ => pa -- using the `example` keyword
NEGATION
There are, of course, only so many theorems we can state using only implication. In the next chapter we will show how the λ-calculus can be extended to include ∧
, ∨
, and False
. To give a sense of how this looks, here is an example using ¬p
, which as you will recall is the same as p → False
.
variable (p q: Prop)
example : p → ¬p → q :=
λ pa pna => absurd pa pna
example : (p → q) → (¬q → ¬p) :=
fun hpq nq hp => absurd (hpq hp) nq
Here, absurd is a theorem from the Lean standard library that we will discuss when we get to Lean's inductive type
system.
A Note About Variable Declarations
If we had used
variable (A C : Prop)
above, then my_lean_theorem would have (A : Prop) as a non-implicit argument, so it would have to be applied as
my_lean_theorem hca h
which is ugly.
The way Lean uses variables is by putting them silently into all definitions and theorems that use them. So my_theorem internally looks like:
theorem my_lean_theorem (A : Prop) : A → A :=
λ proof : A => proof
On the other hand, if we use curly braces in the variable declaration, as we did in the previous examples, then we get
theorem my_lean_theorem {A : Prop} : A → A :=
λ proof : A => proof
so that the type of A is an implicit argument to my_lean_theorem.
References
Morten Heine Sørensen, Pawel Urzyczyn "Lectures on the Curry-Howard Isomorphism" Elsevier. 1st Edition, Volume 149 - July 4, 2006.
- Chapter 4 describes Intuitionistic Propositional Logic
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Inductive Types
As we saw in the chapter on the λ-Calculus, we can encode fairly sophisticated objects like the natural numbers using just abstractions and applications. However, such encodings are a best clunky and hard to read. Additionally, encoding complex data types as λ-Calculus expressions has other problems:
Noncanonical terms: The types of such encodings are not guaranteed to result in canonical terms. For example, Church numerals were defined to have type
def α := Type
def N := (α → α) → α → α
But we can define expressions that have this type but that do not correspond to natural numbers. For example,
def nc : N := λ (f : α → α) (x : α) => x
It would be vastly preferable if (a) every object of a given type was a legitimate representative of that type and every object also had exactly one representative.
Pattern Matching and Induction: To prove properties above objects of a given type, it is useful to apply induction on the structure of the object. For example, a natural number is either zero, or it is the successor of some other natural number. To prove a statement about natural numbers one would like support for pattern matching on the way the number was constructed.
Termination: As we have seem, operations on terms of a given type in the pure lambda calculus are not guaranteed to terminate. However, we will see that all terms of a given inductive type support structural recursion: We can define functions on that break the term into smaller pieces which eventual lead to indivisible elements, at which point the function terminates.
Thus, Lean and other type theoretic languages include a way to define types inductively. One lists all the ways to construct objects of a given type. Lean then provides a powerful pattern matching capability that can be used in definitions and theorems when operating or reasoning on an object defined inductively.
Namespaces
In this chapter we will be redefining several fundamental types in Lean, such as the natural numbers ℕ
and the propositional connectives And
and Or
. Since these are part of Lean's standard library (included by default), if we do not take appropriate measures, we will get naming collisions. The easiest way to avoid this is to open a temporary namespace.
namespace Temp
Now, when we define a new symbol, such as
def Thing := Type
we are actually defining Temp.Thing. If Thing is defined in some inluded library, our new definition will not collide with it.
Lean's Inductive Types
So far we have introduced only simple arrow types composed Lean's basic type (called Type) and functions from those types into types. We now introduce a powerful way to make new types, which covers almost all of mathematics, called inductive types.
An inductive type is generated by constructors that may refer to the type itself. They say how to make objects of the given type.
Example: A type with only two elements is defined by:
inductive Two where
| a : Two
| b : Two
#check Two.a
#check Two.b
def t := Two.a
#eval t
Example: The simplest inductive yype has no constructors, meaning it specifies the empty type.
inductive Empty
Constructors With Arguments
You can also have constructors that take arguments and transform them into objects of the given type.
Example: The type Nat of Natural Numbers is defined by two constructors:
inductive Nat where
| zero : Nat
| succ : Nat → Nat -- succ stand for `successor`
open Nat
#check succ (succ (succ zero)) -- 3
All the constructors in an inductively defined type live in a namespace with the same name as the type. The open command allows us to write succ instead of Nat.succ. We can also write
#check zero.succ.succ.succ
using so-called dot-notation.
Objects of type Nat
thus either have the form zero
or they consist of some finite number of applications of succ
to the element zero
. With more types, we can define even more complicated objects.
Example: A simple model of arithmetic expressions can be defined by the type:
inductive Expr where
| var : String → Expr
| add : Expr → Expr → Expr
| mul : Expr → Expr → Expr
| neg : Expr → Expr
open Expr
Some example terms include
#check add (var "x") (var "y") -- x+y
#check add (var "x") (mul (neg (var "y")) (var "z")) -- x-yz
Functions of Inductive Types
To work with objects of inductive types, we usually need to know how the object was constructed. Lean uses the keyword match
for that.
Example: Toggling a Two
def Two.toggle ( x : Two ) := match x with
| a => b
| b => a
Lean also knows how to reduce expressions involving match.
open Two
#reduce toggle (toggle a)
#reduce a.toggle.toggle
Example: 1+1 = 2
def Nat.plus (n m : Nat) := match n with
| zero => m
| succ x => succ (plus x m)
open Nat
#reduce plus (succ zero) (succ zero)
Example: Swap Adds and Muls
def Expr.swap (e : Expr) := match e with
| var s => var s
| add x y => add y x
| mul x y => mul y x
| neg x => neg x
def e := add (var "x") (mul (neg (var "y")) (var "z"))
#reduce e.swap -- -zy+x
Inductive Types May Depend on Other Types
The types we have defined so far do not interact with other types. Here's an example that does: Lists of Nats.
inductive NatList where
| empty : NatList
| cons : Nat → NatList → NatList
namespace NatList
#check cons zero (cons zero empty) -- [0, 0]
#check (empty.cons zero).cons zero -- [0, 0]
end NatList
#check [1,2]
Or we can define a List of elements of any type. In the the next bit of code, we implicitly state (using curly braced instead of parens) that List depends on an arbitrary type α.
inductive List {α : Type} where
| empty : List
| cons : α → List → List
namespace List
#check cons "lean" (cons "is cool" empty) -- ["lean", "is cool"]
#check cons 3.4 (cons 1.21 empty) -- ["lean", "is cool"]
end List
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Propositional Logic Connectives
One of the remarkable things about inductive types is that the capture all of propositional logic, first order logic, and more. Thus, instead of defining and, or and the other logical connectives as built-in operators in the Lean language, they are just defined in the standard library in terms of more primited inductive types.
namespace Temp
And is an Inductive Type
Recall the inference rule
Γ ⊢ φ Γ ⊢ ψ
∧-Intro ———————————————————
Γ ⊢ φ ∧ ψ
It states that whenever we know propositions φ and ψ, then we know φ ∧ ψ. From the point of view of types, it says that if φ and ψ are of type Prop, then so is φ ∧ ψ. In Lean we can write this as an inductive type definition as follows.
inductive And (φ ψ : Prop) : Prop where
| intro : φ → ψ → And φ ψ
You can think of h : And p q
as
- h has type And p q
- h is evidence that the type And p q is not empty
- h is a proof of the proposition And p q.
A Proof of a Simple Proposition
Consider the proposition
p → q → And p q
As a type, this proposition is a function from p to q to And p q. Thus, we know that an element of this type has the form
λ hp => λ hq => sorry
For the body of this lambda abstraction, we need to introduce
an And type, which requires proofs of p and q respectively. Using the inductive definition of And we get
λ hp hq => And.intro hp hq
def g (p q : Prop) : p → q → And p q :=
λ hp => λ hq => And.intro hp hq
And Eliminiation
The elimination rules for And are
Γ ⊢ φ ∧ ψ Γ ⊢ φ ∧ ψ
∧-Elim-Left —————————————— ∧-Elim-Right —————————————
Γ ⊢ φ Γ ⊢ ψ
which we can write in Lean as
def And.left {p q : Prop} (hpq : And p q) :=
match hpq with
| And.intro hp _ => hp
def And.right {p q : Prop} (hpq : And p q) :=
match hpq with
| And.intro _ hq => hq
Proofs with And-Elimination
With these inference rules, we can do even more proofs:
example (p q : Prop) : (And p q) → p :=
λ hpq => And.left hpq
example (p q : Prop) : (And p q) → (And q p) :=
λ hpq => And.intro hpq.right hpq.left
Match is Enough
Note that the elimination rules above are a convenience we defined to make the proof look more like propositional logic. We could also just write:
example (p q : Prop) : (And p q) → p :=
λ hpq => match hpq with
| And.intro hp _ => hp
This pattern suggests that with inductive types, we can think of match as a generic elimination rule.
Or is Inductive
To introduce new OR propositions, we use the two introduction rules
Γ ⊢ φ Γ ⊢ ψ
∨-Intro-Left ——————————— ∨-Intro-Right ————————————
Γ ⊢ φ ∨ ψ Γ ⊢ φ ∨ ψ
In Lean, we have
inductive Or (φ ψ : Prop) : Prop where
| inl (h : φ) : Or φ ψ
| inr (h : ψ) : Or φ ψ
And we can use this inference rule in proofs as well.
example (p q : Prop) : And p q → Or p q :=
λ hpq => Or.inr hpq.right
Or Elimination
Recall the inference rule
Γ,p ⊢ r Γ,q ⊢ r Γ ⊢ p ∨ q
∨-Elim ————————————————————————————————————
Γ ⊢ r
It allows us to prove r given proofs that p → r
, q → r
and p ∨ q
. We can define this rule in Lean with:
def Or.elim {p q r : Prop} (hpq : Or p q) (hpr : p → r) (hqr : q → r) :=
match hpq with
| Or.inl hp => hpr hp
| Or.inr hq => hqr hq
Example of and Or-Elim Proof
Here is an example proof using introduction and elimination.
example (p q : Prop): Or p q → Or q p :=
λ hpq => Or.elim
hpq -- p ∨ q
(λ hp => Or.inr hp) -- p → (q ∨ p)
(λ hq => Or.inl hq) -- q → (q ∨ p)
Once again, the elimination rule is just a convenience and the proof could be written with match.
False is Inductive
Finally, we have False
, which has no introduction rule, kind of like Empty
, except we add the requirement that False
is also type of Prop
.
inductive False : Prop
From False we get the Not
connective, which is just "syntactic sugar".
def Not (p : Prop) : Prop := p → False
Here is an example proof:
example (p q : Prop): (p → q) → (Not q → Not p) :=
λ hpq hq => λ hp => hq (hpq hp)
False Elimination
To define the elimination rule for false
Γ ⊢ ⊥
⊥-Elim ——————————
Γ ⊢ p
we take advantage of the fact that False was defined inductively.
def False.elim { p : Prop } (h : False) : p :=
nomatch h
Here is an example proof that from False you can conclude anything:
example (p q : Prop): And p (Not p) → q :=
λ h => False.elim (h.right h.left)
By the way, this is another way to prove the HW1 example:
example : False → True :=
λ h => False.elim h
Notation
The main difference between what we have defined here and Lean is that Lean defines notation like ∨
and ∧
. We won't redo that entire infrastructure here. But to give a sense of it, here is how Lean defines infix notation for Or and And and Not notation.
infixr:30 " ∨ " => Temp.Or
infixr:35 " ∧ " => Temp.And
notation:max "¬" p:40 => Temp.Not p
The numbers define the precedence of the operations. So v
has lower precedence than ∧
, which has lower precedence than -
.
Now we can write
end Temp -- start using Lean's propositions
example (p q : Prop): (p ∧ (¬p)) → q :=
λ h => False.elim (h.right h.left)
Examples
You should try to do as many of these as possible. These are borrowed from the Theorem Proving in Lean Book.
variable (p q r : Prop)
example (h : p ∨ q) : q ∨ p := sorry
example : p ∧ q ↔ q ∧ p := sorry
example : p ∨ q ↔ q ∨ p := sorry
example : (p ∨ q) ∨ r ↔ p ∨ (q ∨ r) := sorry
example : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := sorry
example : p ∨ (q ∧ r) ↔ (p ∨ q) ∧ (p ∨ r) := sorry
example : (p → (q → r)) ↔ (p ∧ q → r) := sorry
example : ((p ∨ q) → r) ↔ (p → r) ∧ (q → r) := sorry
example : ¬(p ∨ q) ↔ ¬p ∧ ¬q := sorry
example : ¬p ∨ ¬q → ¬(p ∧ q) := sorry
example : ¬(p ∧ ¬p) := sorry
example : p ∧ ¬q → ¬(p → q) := sorry
example : ¬p → (p → q) := sorry
example : (¬p ∨ q) → (p → q) := sorry
example : p ∨ False ↔ p := sorry
example : p ∧ False ↔ False := sorry
example : (p → q) → (¬q → ¬p) := sorry
example : (p → q) → (¬q → ¬p) := sorry
References
-
https://lean-lang.org/theorem_proving_in_lean4/inductive_types.html
-
Homotypy Type Theory Book https://homotopytypetheory.org/book/ Chapter 5 covers inductive types
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
First Order Logic
Limitations of Propositional Logic
The main thing missing from propositional logic is objects. For example, suppose we wanted reason about statements like:
- Every person who lives in Seattle lives in Washington.
- There exists a person who does not live in Seattle.
These statements would be difficult in propositional logic, although given that there are only a finite number of people in the world we could say things like:
- lives_in_seattle_eric → lives_in_washington_eric
- lives_in_seattle_fred → lives_in_washington_fred
- ...
where we create new propositions for every person and every statement we would like to say about that person. However, what if we wanted to reason about an infinite domain like ℕ and say things like the following?
- every natural number is either odd or even
Since there are an infinite number of natural numbers, we need an infinite number of propositions
- odd_0, even_0, odd_1, even_1, ...
First Order Logic
First order logic (FOL) enriches propositional logic with the following elements:
- Objects: such as numbers, names, people, places, etc.
- Functions: that transform objects into other objects -- See next set of notes
- Predicates: that relate objects to objects
- Quantifiers: ∀ and ∃ that allow us to say:
- ∀: For all objects ___
- ∃: There exists an object such that ___
- All the connectives we have encountered so far: ∨, ∧, →, ¬, ...
- Types: Traditional FOL does not have types, but we will use them anyway)
For example, in the following proposition built from these elements:
∀ x ∃ y , f x > y
is read "For all x, there exists a y such that f(x) is greater than y". In this example,
- The objects x and y are presumbly numbers
- The symbol f is a function that maps numbers to numbers
- The symbol > is predicate taking two arguments and return true or false
All of this can be done easily in Lean.
variable (f : Nat → Nat)
#check ∀ x : Nat , ∃ y : Nat , f x > y
Objects
Objects in FOL can come from any agreed upon universe. Since we will be using Lean to work with first order logic, you can just assume that objects are any basic terms: numbers, strings, lists, and so on. FOL does not allow us to quantify over functions and types, only basic objects.
Example: A Finite Universe of People
For example, suppose we wanted to reason about a finite number of people. In Lean we can enumerate them with a new type:
inductive Person where | mary | steve | ed | jolin
open Person
#check ed
Example : Natural Numbers, Strings, Booleans, etc
Lean has a number of built inn types we can use, such as numbers, strings, and Booleans.
#check 1234
#check "uwece"
#check true
Predicates
A predicate is a Prop
valued function.
Example: A Predicate on People
A predicate on Person is a function from Person into Prop, such as one which might specify whether the person lives in Seattle:
def InSeattle (x : Person) : Prop := match x with
| mary | ed => True
| steve | jolin => False
#check InSeattle
example : InSeattle steve ∨ ¬InSeattle steve :=
Or.inr (λ h => h)
Example: A Predicate on ℕ
Or we might define a predicate inductively on the natural numbers.
def is_zero(n : Nat) : Prop := match n with
| Nat.zero => True
| Nat.succ _ => False
#check is_zero
example : ¬is_zero 91 := -- is_zero 91 → False
λ h => h
theorem t : is_zero 0 := True.intro
theorem t1 : True := True.intro
Predicates with Multiple Arguments
We may define predicates to take any number or arguments, including no arguments at all.
-- No argument predicates are just normal propositions
variable (P : Prop)
#check P
-- A one-argument predicate
variable (InWashington : Person → Prop)
#check InWashington steve
-- A two-argument predicate
variable (Age : Person → Nat → Prop)
#check Age jolin 27
Relations
A two-argument predicate is called a relation.
Example: We might define a predicate on pairs of people such as
def on_right (p q : Person) : Prop := match p with
| mary => q = steve
| steve => q = ed
| ed => q = jolin
| jolin => q = mary
We can define other predicates in terms of existing predicates.
def next_to (p q : Person) := on_right p q ∨ on_right q p
example : next_to mary steve :=
Or.inl (Eq.refl steve)
Greater Than is a Relation
Relations are usually represented with infix notation, but they are still just predicates. For example, in Lean, the greater-than relation on natural numbers is:
#check @GT.gt Nat
#eval GT.gt 2 3
This doesn't look very nice, so Lean defines notation:
infix:50 " > " => GT.gt
and we can write:
#eval 2 > 3
Similarly, >=, <, <=, != are all relations available in Lean.
Universal Quantification
In FOL, we use the symbol ∀ to denote universal quantification. You can think of univeral quantifiaction like a potentially infinte AND:
∀ x P(x) ≡ P(x₁) ∧ P(x₂) ∧ P(x₃) ∧ ...
Example: Here's how you say "All people who live in Seattle also live in Washington":
∀ x : Person , InSeattle(x) → InWashington(x)
Example: In Lean, let's say we wanted to prove that every person either lives in Seattle or does not live in Seattle. A proof of this fact has the form of a function that takes an arbtrary person x and returns a proof that that person either lives in Seattle or does not. Thus, we can say:
example : ∀ (x : Person) , (InSeattle x) ∨ ¬(InSeattle x) :=
λ x => match x with
| steve => Or.inr (λ h => h)
| mary => sorry
| ed => sorry
| jolin => sorry
∀ is just syntactic sugar for polymorphism. The above FOL statement can be equally well written as:
#check (x : Person) → (InSeattle x) ∨ ¬(InSeattle x)
Which highlights why we can just use a lambda to dispatch a forall.
Forall Introduction and Elimination
The universal quantifer has the introduction rule:
Γ ⊢ P
∀-intro ————————————————————————
Γ ⊢ ∀ x : α, P
Where x is not in the free variables of Γ
. The rule states that if we can prove P
in context Γ
assuming x
not mentioned elsewhere in Γ
, then we can prove ∀ x : α, P
.
We also have the elimination rule:
Γ ⊢ ∀ x , P x
∃-elim ————————————————————————
P t
where t
is any term. This rule states that if we know P x
holds for every x
, then it must hold for any particular t
.
Proving Statements with ∀
The Curry-Howard Isomorphism works for universal quantification too. We could do as we did with proposotional logic and rewrite the FOL rules as type inference. However, here we just say what it means in Lean (which amounts to the same thing).
-
∀-intro: To prove
∀ x , P x
we construction a function that takes anyx
and returns proof ofP x
. This is an extension of the λ-abstraction rule. -
∀-elim: Given a proof
h
of∀ x , P x
(which we recall is a λ-abstractionn) and a particulary
of typeα
, we can proveP y
by simply applyingh
toy
. This is an extension of the λ-application rule.
For example, here is a proof that uses both of these rules:
variable (α : Type) (P Q : α → Prop)
example : (∀ x : α, P x ∧ Q x) → ∀ y : α, P y :=
λ h q => (h q).left
Exists
The ∃
quantifer is like an OR over a lot of propositions:
∃ x , P(x) ≡ P(x₁) ∨ P(x₂) ∨ ....
and it has similar introduction and elimination rules:
Γ ⊢ φ[x:=t] Γ ⊢ ∃ x , φ Γ, φ ⊢ ψ
∃-intro: ——————————————— ∃-elim: ———————————————————————————
Γ ⊢ ∃ x, φ Γ ⊢ ψ
Constructively, the first rule says that if we have a proof of φ
with x
some term t
substituted in for x
, then we have a proof of ∃ x, φ
.
The second says that if we have a proof of ∃ x, φ
and also a proof of ψ
assuming φ
, then we have a proof of ψ.
Lean's Implementation of Exists
In FOL, ∃ is usally just an abbreviation for as ¬∀¬
. However, from a constructive point of view:
knowing that it is not the case that every
x
satisfies¬p
is not the same as having a particularx
that satisfies p. (Lean manual)
So in Lean, ∃
is defined inductively and constructively:
namespace temp
inductive Exists {α : Type} (p : α → Prop) : Prop where
| intro (x : α) (h : p x) : Exists p
end temp
All we need to introduce an existentially quantified statement with predicate P
is an element and a proof that P
holds for that element.
An example use of the introduction rule is the following. Note the assumption that α has at least one element q
is necessary.
example (q : α) : (∀ x , P x) → (∃ x , P x) :=
λ hp => Exists.intro q (hp q)
Exists Elimination
The ∃-elim rule is defined in Lean as follows:
namespace temp
theorem Exists.elim {α : Type} {P : α → Prop} {b : Prop}
(h₁ : Exists (λ x => P x)) (h₂ : ∀ (a : α), P a → b) : b :=
match h₁ with
| intro a h => h₂ a h
end temp
In this rule
b is an arbitrary proposition h₁ is a proof of ∃ x , p x h₂ is a proof that ∀ a , p a → b
which allow us to conclude b
Exists Elimination Example
For example, in
example (h₁ : ∃ x, P x ∧ Q x) : ∃ x, Q x ∧ P x :=
Exists.elim h₁
(λ c h => Exists.intro c (And.intro h.right h.left))
Example Proofs
variable (p: Type → Prop)
variable (r : Prop)
example : (∃ x, p x ∧ r) ↔ (∃ x, p x) ∧ r :=
Iff.intro
(λ h => Exists.elim h (λ c h => And.intro (Exists.intro c h.left) h.right))
(λ h => Exists.elim h.left (λ c h1 => Exists.intro c (And.intro h1 h.right)))
example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) :=
Iff.intro
(λ h x hp => h (Exists.intro x hp))
(λ h he => Exists.elim he (λ y hy => h y hy))
example : ∀ (x : Person) , (InSeattle x) ∨ ¬(InSeattle x) :=
λ x => match x with
| mary | ed => Or.inl trivial
| steve | jolin => Or.inr (λ h => False.elim h)
example : (∀ x : α, P x ∧ Q x) → ∀ y : α, P y :=
λ h : ∀ x : α, P x ∧ Q x =>
λ y : α =>
(h y).left
example (q : α) : (∀ x , P x) → (∃ x , P x) :=
λ h => Exists.intro q (h q)
example (h₁ : ∃ x, P x ∧ Q x) : ∃ x, Q x ∧ P x :=
have h₂ := λ w : α => -- proof of ∀
λ hpq : P w ∧ Q w => -- proof of →
(Exists.intro w (And.intro hpq.right hpq.left))
Exists.elim h₁ h₂
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Tactics
Tactic mode is entered in a proof using the keyword by
variable (p : Type → Prop)
example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := by
sorry
The intro
Tactic
Introducion applies to implications and forall statements, introducing either a new hypothesis or a new object. It takes the place of λ h₁ h₂ ... => ...
Note also that by using .
and indentation, you can visually break up your proof to it is more readable.
example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := by
apply Iff.intro
. intro hnep x
sorry
. intro hanp
sorry
The apply
and exact
Tactics
The apply
tactic applies a function, forall statement, or another theorem. It looks for arguments that match its type signature in the context and automatically uses them if possible.
example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := by
apply Iff.intro
. intro h x hp
exact h (Exists.intro x hp)
. intro h hepx
apply Exists.elim hepx
intro x hpa
exact (h x) hpa
example (p : Nat → Prop) (h : ∀ (x : Nat) , p x) : p 14 := by
apply h
theorem my_thm (q : Prop) : q → q := id
example (q : Nat → Prop) : (∀ x, q x) → ∀ x, q x := by
apply my_thm
exact
is a variant of apply that requires you to fill in the arguments you are using. It essentially pops you out of tactic mode. It is used at the end of proofs to make things more clear and robust to changes in how other tactics in the proof are applied.
example (p : Nat → Prop) (h : ∀ (x : Nat) , p x) : p 14 := by
exact h 14
The assumption
Tactic
This tactic looks through the context to find an assumption that applies, and applies it. It is like apply but where you don't even say what to apply.
example (c : Type) (h : p c) : ∃ x, p x := by
apply Exists.intro c
assumption
Structures
Structures in Lean are a way to package data. They are a kind of inductive type, but presented differently. For example,
structure Point where
x : Int
y : Int
You can make new points in a variety of ways
def p₁ := Point.mk 1 2
def p₂ : Point := { x := 1, y := 2 }
def p₃ : Point := ⟨ 1,2 ⟩
Packaging and Exists
In Lean, And is a structure (not a simple inductive type, like I originally described).
#print And
example (p : Prop): p → (p ∧ p) :=
λ hp => ⟨ hp, hp ⟩
This notation also works with inductive types though, as with Exists.
#print Exists
example (p : Type → Prop) (c : Type) : (∀ x, p x) → ∃ x, p x :=
λ h => ⟨ c, h c ⟩
example : ∃ (p : Point) , p.x = 0 := by
exact ⟨ ⟨ 0, 0 ⟩, rfl ⟩
Tactics Produce Low Level Proofs
theorem t (p : Type → Prop) (c : Type) : (∀ x, p x) → ∃ x, p x := by
intro h
exact ⟨ c, h c ⟩
#print t
Pattern Matching
You can match constructors with intro to more easily break up expressions.
example (p q : Prop) : p ∧ q → q := by
intro ⟨ _, hq ⟩
exact hq
example : (∃ x , ¬p x) → ¬ ∀ x, p x := by
intro ⟨ x, hnp ⟩ hnap
exact hnp (hnap x)
example (P Q : Type → Prop): (∃ x, P x ∧ Q x) → ∃ x, Q x ∧ P x := by
intro ⟨ x, ⟨ hp, hq ⟩ ⟩
exact ⟨ x, ⟨ hq, hp ⟩ ⟩
Getting Help with Apply?
You can ask Lean to try to find someting to apply with apply?
example : (∃ x , ¬p x) → ¬ ∀ x, p x := by
intro ⟨ x, hnp ⟩ hnap
apply?
It doesn't always work though.
FOL Examples Revisited
Now that we can use tactics, our First Order Logic Proofs can be made to look a little cleaner, although one might argue the use of angled brackets is harder to read.
variable (p: Type → Prop)
variable (r : Prop)
theorem asd : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := by
apply Iff.intro
. intro h x hp
exact h (Exists.intro x hp)
. intro hp ⟨ x, hnp ⟩
exact hp x hnp
example : (∃ x, p x ∧ r) ↔ (∃ x, p x) ∧ r := by
apply Iff.intro
. intro ⟨ x, ⟨ hx, hr ⟩ ⟩
exact ⟨ ⟨ x, hx ⟩ , hr ⟩
. intro ⟨ ⟨ x, hx ⟩ , hr ⟩
exact ⟨ x, ⟨ hx, hr ⟩ ⟩
example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := by
apply Iff.intro
. intro h x hp
exact h ⟨ x, hp ⟩
. intro h ⟨ x, hp ⟩
exact h x hp
The have
and let
Tactics
You can use have
to record intermediate results
example (p q : Prop) : p ∧ q → p ∨ q := by
intro ⟨ h1, h2 ⟩
have hp : p := h1
exact Or.inl hp
If you need an intermediate value, you should use let
.
example : ∃ n , n > 0 := by
let m := 1
exact ⟨ m, Nat.one_pos ⟩
Cases
The cases tactic wraps around Or.elim to make proofs easier to read.
example (p q : Prop) : (p ∨ q) → q ∨ p := by
intro h
cases h with
| inl hp => exact Or.inr hp
| inr hq => exact Or.symm (Or.inr hq)
-- Cases doesn't always buy you much. You can just apply Or.elim.
example (p q : Prop) : (p ∨ q) → q ∨ p := by
intro h
apply Or.elim h
. intro hp
exact Or.symm h
. intro hq
exact Or.symm h
Cases Works With any Inductive Ttype
Here's are some somewhat longwinded ways to prove some simple results
variable (P Q : Type → Prop)
example : (∃ x, P x ∧ Q x) → ∃ x, Q x ∧ P x := by
intro h
cases h with
| intro x h => exact ⟨ x, And.symm h ⟩
example (p q : Prop) : (p ∧ q) → (p ∨ q) := by
intro h
cases h with
| intro hp hq => exact Or.inl hp
The by_cases
Tactic
The cases tactic is not to be confused with the by_cases
tactic, which uses classical reasoning
.
example (p : Prop): p ∨ ¬p := by
by_cases h : p
. exact Classical.em p -- assuming h : p
. exact Classical.em p -- assuming h : ¬p
The induction
Tactic
Proof by induction works for all inductive types. It is similar to using cases, but it adds an inductive hypothesis
where needed.
As an example, consider the natural numbers and suppose P : Nat → Prop is a property. To prove P with induction, you do :
- BASE CASE: P(0)
- INDUCTIVE STEP: ∀ n, P(n) → P(n+1)
def E (n : Nat) : Prop := match n with
| Nat.zero => True
| Nat.succ x => ¬E x
example : ∀ n : Nat, E n ∨ E n.succ := by
intro n
induction n with
| zero => exact Or.inl trivial
| succ k ih =>
apply Or.elim ih
. intro h1
exact Or.inr (by exact fun a ↦ a h1)
. intro h3
exact Or.inl h3
Tactic Documentation
There are a lot of tactics:
https://github.com/haruhisa-enomoto/mathlib4-all-tactics/blob/main/all-tactics.md
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Objects, Functions and Equality
In this chapter we extend the first order logic discussed in the last chapter to deal with functions of objects in our universe. On one of the critical components is a notion of equality between objects. Astonishingly, Lean's equality is not a built in type, but is defined in the standard library. Once we have equality, we can start working with statements about functions and their relationships in earnest.
Equality is a Binary Relation Defined Inductively
universe u
inductive MyEq {α : Sort u} : α → α → Prop where
| refl a : MyEq a a
#check MyEq 1 2
example : MyEq 1 1 :=
MyEq.refl 1
We can define some notation
infix:50 " ~ " => MyEq
#check 1 ~ 1
Refl is Powerful
In Lean, terms that are beta-reducable to each other are considered definitionally equal. You can show a lot of equalities automatically
example : 1 ~ 1 :=
MyEq.refl 1
example : 2 ~ (1+1) := by
apply MyEq.refl
example : 9 ~ (3*(2+1)) := by
apply MyEq.refl
Substitution
Substition is the second most critical property of the equality. It allows us to conclude, for example, that if x = y then p x is equal to p y.
theorem MyEq.subst {α : Sort u} {P : α → Prop} {a b : α}
(h₁ : a ~ b) (h₂ : P a) : P b := by
cases h₁ with
| refl => exact h₂
You can use this theorem to show the standard properties we know and love about equality.
theorem my_symmetry (a b : Type): a ~ b → b ~ a := by
intro h
apply MyEq.subst h
exact MyEq.refl a
theorem my_transitivity (a b c : Type) : a ~ b → b ~ c → a ~ c := by
intro hab hbc
exact MyEq.subst hbc hab
theorem my_congr_arg (a b : Type) (f : Type → Type) : a ~ b → f a ~ f b := by
intro hab
apply MyEq.subst hab
exact MyEq.refl (f a)
Lean's Equality
Lean's equality relation is called Eq
and its notation is =
, as we have been using. Lean also defines rfl
to be Eq.refl _
#print rfl
example : 9 = 3*(2+1) := Eq.refl 9
example : 9 = 3*(2+1) := rfl
Lean provides a long list of theorems about equality, such as
#check Eq.symm
#check Eq.subst
#check Eq.substr
#check Eq.trans
#check Eq.to_iff
#check Eq.mp
#check Eq.mpr
#check congrArg
#check congrFun
#check congr
Tactics for Equality
rw[h]: Rewrites the current goal using the equality h.
theorem t1 (a b : Nat) : a = b → a + 1 = b + 1 := by
intro hab
rw[hab]
#print t1
To use an equality backwards, use ← (written \left)
theorem t2 (a b c : Nat) : a = b ∧ a = c → b + 1 = c + 1 := by
intro ⟨ h1, h2 ⟩
rw[←h1, ←h2]
#print t2
You can also rewrite assumptions using at
.
example (a b c : Nat) : a = b ∧ a = c → b + 1 = c + 1 := by
intro ⟨ h1, h2 ⟩
rw[h1] at h2
rw[h2]
The Simplifier
The simplifier uses equations and lemmas to simplify expressions
theorem t3 (a b : Nat) : a = b → a + 1 = b + 1 := by
simp
#print t3
Sometimes you have to tell the simplifer what equations to use.
theorem t4 (a b c d e : Nat)
(h1 : a = b)
(h2 : b = c + 1)
(h3 : c = d)
(h4 : e = 1 + d)
: a = e := by
simp only[h1,h2,h3,h4,Nat.add_comm]
#check Nat.add_comm
#print t4
The linarith
Tactic
By importing Mathlib.Tactic.Linarith (see top of this file), you get an even more powerful simplifier.
example (a b c d e : Nat)
(h1 : a = b)
(h2 : b = c + 1)
(h3 : c = d)
(h4 : e = 1 + d)
: a = e := by linarith
example (x y z : ℚ)
(h1 : 2*x - y + 3*z = 9)
(h2 : x - 3*y - 2*z = 0)
(h3 : 3*x + 2*y -z = -1)
: x = 1 ∧ y = -1 ∧ z = 2 := by
apply And.intro
. linarith
. apply And.intro
. linarith
. linarith
Example : Induction on Nat
As an example the brings many of these ideas together, consider the sum of the first n
natural numbers, which is n(n+1)/2
. A proof by induction would be:
- BASE CASE:
0 = 0*1/2
- NDUCTIVE STEP:
∀ k, Sum k = k(k+1)/2 → Sum (k+1) = (k+1)(k+2)/2
We can do this in lean with the induction
tactic.
def S (n : Nat) : Nat := match n with
| Nat.zero => 0
| Nat.succ x => n + S x
#eval S 3
example : ∀ n, 2 * S n = n*(n+1) := by
intro n
induction n with
| zero => simp[S]
| succ k ih =>
simp[S,ih]
linarith
Inequality
Every inductive type comes with a theorem called noConfusion that states that different constructors give different objects.
inductive Person where | mary | steve | ed | jolin
open Person
example : mary ≠ steve := by
intro h
exact noConfusion h
inductive MyNat where
| zero : MyNat
| succ : MyNat → MyNat
example : MyNat.zero ≠ MyNat.zero.succ := by
intro h
exact MyNat.noConfusion h
Continuing the above example, suppose we want to specify who is on who's right side.
def on_right (p : Person) := match p with
| mary => steve
| steve => ed
| ed => jolin
| jolin => mary
def next_to (p q : Person) := on_right p = q ∨ on_right q = p
example : ¬next_to mary ed := by
intro h
cases h with
| inl hme => exact noConfusion hme
| inr hem => exact noConfusion hem
example : ∀ p , p ≠ on_right p := by
sorry
Note: The trivial
tactic actually figures out when to apply noConfusion
theorem t10 : ed ≠ steve := by
intro h
trivial
#print t10
#help tactic trivial
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Relations
As described previously, a relation is a propositionally valued predicate of two arguments. Generally speaking, that is about all you can say about predicates. However, when we restrict our attention to predicates having specific properties, we can say much more.
In this chapter we will build up some of the theory behind relations and give several examples of each type of relation.
Note that Mathlib has many definitions involving relations. In particular, Rel
is the general type of relations. We will not use that infrastructure in this chapter, as our goal is to build up the theory from scratch for the purposes of understanding it better, which in turn should make Mathlib more comprehensible.
Definitions
First, we define a general type for relations:
abbrev Relation (A : Type u) (B : Type v) := A → B → Prop
Types of Relation
abbrev Refl {A : Type u} (r : Relation A A) {x : A} := r x x
abbrev Symm {A : Type u} (r : Relation A A) {x y : A} := r x y → r y x
abbrev AntiSym {A : Type u} (r : Relation A A) {x y : A}:= r x y → r y x → x = y
abbrev Trans {A : Type u} (r : Relation A A) {x y z : A} := r x y → r y z → r x z
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
TODO
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
The Natural Numbers and Hierarchies
In the chapter on inductive types, we encountered the natural numbers. So to some extent, we are done defining them. But the natural numbers have many, many properties that are incredibly useful over a huge range of mathematics. In fact, defining them is the easy part. Understanding their structure is much more interesting.
In this chapter, we will define the natural numbers and develop some of their algebraic and ordering structure. Along the way, we show how Lean's hierarchy system works. Hierarchies are useful for proving general theorems about algebreic structures that can be reused in specific instances. For example, consider associative property: (x+y)+z = x+(y+z)
. This property holds for natural numbers, integers, rationals, reals, matrices, polynomials, and many more objects. And it leads to many auxilliary theorems, such as (w+x)+(y+z) = (w+(x+y))+z
and so on. Rather than proving all all these theorems for a new type, we just prove a few basic theorems, like associativity and a few others, and then do some book-keeping to connect our new type to the hige list of theorems that hold for similar objects.
The Inductive Definition of the Natural Numbers
As we've seen, the Natural Numbers are defined inductively. We open the a temporary namespace, Temp
to avoid naming conflicts with Lean's standard Nat
type. So, in the below, every time you see Nat
, it means Temp.Nat
.
#check Nat.succ_add
theorem succ_add_eq_add_succ' (a b : Nat) : Nat.succ a + b = a + Nat.succ b := by
apply Nat.succ_add
example (j k: Nat) : j.succ + k = j + k.succ := by exact Nat.succ_add_eq_add_succ j k
#check Nat.succ_add_eq_add_succ
namespace Temp
-- Definition
inductive Nat where
| zero : Nat
| succ : Nat → Nat
open Nat
Using this definition, it is straightfoward to define addition and multiplication.
def Nat.add (n m: Nat) : Nat := match m with
| zero => n
| succ k => succ (add n k)
-- def Nat.add (n m: Nat) : Nat := match n,m with
-- | a, Nat.zero => a
-- | a, Nat.succ b => Nat.succ (Nat.add n m)
def Nat.mul (n m: Nat) : Nat := match n with
| zero => zero
| succ k => add (mul k m) m -- (k+1)*m = k*m+m
First Use of Lean's Classes
If you have a type for which things like zero, one, addition, and multiplication are defined, it would be nice to use the notation 0, 1, + and *. Although you could use Lean's syntax
and infix
operators to define such notation, it is better practice to instantiate the classes that group all types that have zero, one, addition and multiplication. To do this, we use the instance
keyword and various classes, such as Zero
, One
, Add
and Mul
defined in Lean's standard library or in Mathlib.
Here are several examples:
instance inst_zero : Zero Nat := ⟨ zero ⟩ -- Zero
instance inst_one : One Nat := ⟨ succ zero ⟩ -- One
instance inst_add : Add Nat := ⟨ add ⟩ -- Addition
instance inst_hadd : HAdd Nat Nat Nat := ⟨ add ⟩ -- Extra hints with addition
instance inst_mul : Mul Nat := ⟨ mul ⟩ -- Multiplication
Now we can do a few examples using the notation. Note that in these examples, we have to give Lean some hints that we are working with our Temp.Nat
type. Otherwise it assumes numbers like 0
and 1
refer to the build in Nat type. We do this by coercing one of the terms in our expressions, as in (1:Nat)
.
example : (1:Nat) + 0 = 1 := rfl
example : (1:Nat) * 0 = 0 := rfl
Properties of Addition and Multiplication
With this notation, we can cleanly express some of the basic properties of the natural numbers and start working on proofs. These theorems may seem very basic, but together they form the basis of automated proof checking with the natural numbers, connecting results about, for example, cryptography, with the type-theoretic foundations of mathematics.
Most of these theorems can be found in Lean's standard library. But it is interesting to reproduce them here to understand how the theory is constructed.
#check AddSemiconjBy.eq
#check congrArg
theorem succ_add (n m : Nat) : (succ n) + m = succ (n + m) := by
induction m with
| zero => rfl
| succ k ih => apply congrArg succ ih
theorem succ_add_eq_add_succ (a b : Nat) : succ a + b = a + succ b := by
apply succ_add
theorem add_zero_zero_add {x: Nat} : 0+x=x+0 := by
induction x with
| zero => rfl
| succ j ih =>
apply congrArg succ ih
theorem zero_add {x : Nat} : 0+x = x := by
induction x with
| zero => rfl
| succ j ih =>
apply congrArg succ ih
theorem add_comm (x y : Nat) : x+y = y+x := by
induction x with
| zero => exact add_zero_zero_add
| succ k ih =>
have : y + k.succ = (y + k).succ := rfl
rw[this,←ih]
have : k + y.succ = (k+y).succ := rfl
rw[←this]
exact succ_add_eq_add_succ k y
theorem add_zero (x : Nat) : x+0 = x := by
rw[add_comm,zero_add]
theorem succ_add_one (x : Nat) : x.succ = x + 1 := by
induction x with
| zero => rfl
| succ k ih =>
conv => lhs; rw[ih]
rfl
theorem add_assoc (x y z : Nat) : x+y+z = (x+y)+z := sorry
Ordering Properties of Nat
def Nat.leb (x y : Nat) : Bool := match x,y with
| zero,zero => true
| succ _,zero => false
| zero,succ _ => true
| succ k, succ j => leb k j
def Nat.le (x y : Nat) : Prop := leb x y = true
instance inst_dec_le : DecidableRel le := by
intro x y
match x with
| zero =>
match y with
| zero => exact isTrue rfl
| succ k => exact isTrue rfl
| succ k =>
match y with
| zero =>
unfold le
exact isFalse Bool.false_ne_true
| succ j =>
unfold le
exact (k.succ.leb j.succ).decEq true
def Nat.lt (x y: Nat) : Prop := le x y ∧ x ≠ y
instance inst_le : LE Nat := ⟨ le ⟩
instance inst_lt : LT Nat := ⟨ lt ⟩
#eval le (1:Nat) 0
def Nat.min (x y: Nat) : Nat := if le x y then x else y
instance inst_min : Min Nat := ⟨ min ⟩
instance inst_max : Max Nat := ⟨ sorry ⟩
instance inst_ord : Ord Nat := ⟨ sorry ⟩
instance inst_preo : Preorder Nat := ⟨ sorry, sorry, sorry ⟩
instance inst_po : PartialOrder Nat := ⟨ sorry ⟩
instance inst_lo : LinearOrder Nat := ⟨
sorry,
by exact inst_dec_le,
sorry,
sorry,
sorry,
sorry,
sorry ⟩
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Integers via Quotients
Now that we have defined the natural numbers Nat
, the next obvious step is to define the integers Int
, which included positive _and_negative whole numbers. This can be done in several ways. For example, the Lean 4 standard library defines integers inductively saying that (a) any natural number is an integer, and (b) the negative successor of an integer is an integer.
Here, mainly to synergystically illustrate the use of quotients, we take a different approach, which is standard in foundational mathematics, although perhaps not as idiomatically type theoretic. We define pairs of natural numbers (p,q)
and use the convention that if p>q
then (p,q)
represents the positive number p-q
. Otherwise, it represents the non-positive number q-p
.
This construction allows for multiple representatives of the same number. Therefore, we define an equivalence ≈
on pairs. We would like to write (p,q) ≈ (r,s)
if and only if p-q=r-s
, but since we are in the process of defining the integers, we do not yet have a notion of negative numbers. Some rearrangement leads to
(p,q) ≈ (r,s) ↔ p+s=q+r
instead.
With this equivalence in place, we define an integer to be the equivalence class corresponding to a given difference. For example,
-2 ≡ { (0,2), (1,3), (2,4), ... }
Although we could define -2 to be the set of such pairs, Lean has a much more powerful tool for such constructions: the quotient. Taking the quotient of the set of pairs with respect to the equivalence relation we have defined allows us to use definitional equality on equivalence classes of pairs. This allows us, for example, to substitute an integer for an equivalent one in an algebreic expression.
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
From Pairs to Integers
As usual when defining a type with the same name as something in the standard library or in Mathlib, we open a namespace to avoid naming conflicts. The Int
type we define in this section has the fully qualified name LeanBook.Int
, and is a totally different type than Lean's Int
type.
namespace LeanBook
Pairs of Natural Numbers
We first define pairs of natural numbers, recording the components of the pair in a simple structure. Then we define the notion of equivalence that will form the basis of the definition of an integer.
@[ext]
structure Pair where
p : Nat
q : Nat
def eq (x y: Pair) : Prop := x.p + y.q = x.q + y.p
Here are a few test cases.
example : eq ⟨1,2⟩ ⟨2,3⟩ := rfl
example : eq ⟨3,2⟩ ⟨20,19⟩ := rfl
example : ¬eq ⟨3,2⟩ ⟨20,23⟩ := by intro h; simp_all[eq]
Equivalence Relations
An equivalence relation ≈
is a relation that is
- reflexive: x ≈ x for all x
- symmetric: x ≈ y implies y ≈ x for all x and y
- transitive: x ≈ y and y ≈ z implies x ≈ z for all x, y and z
The relation eq
defined above is such an equivalence relation. But we have to prove it. This is pretty easy, since it is just some basic arithmetic.
theorem eq_refl (u : Pair) : eq u u := by
simp[eq]
linarith
theorem eq_symm {v w: Pair} : eq v w → eq w v := by
intro h
simp_all[eq]
linarith
theorem eq_trans {u v w: Pair} : eq u v → eq v w → eq u w := by
intro h1 h2
simp_all[eq]
linarith
With these properties in hand, we can register an instance of Equivalence
, a Lean 4 standard library class that stores the properties of the equivalence relation in one object, and enables us to easily use any theorems requiring our eq
relation to have them.
instance eq_equiv : Equivalence eq := ⟨ eq_refl, eq_symm, eq_trans ⟩
We can also register eq
with the HasEquiv
class, which allows us to use the `≈' notation.
@[simp]
instance pre_int_has_equiv : HasEquiv Pair := ⟨ eq ⟩
Here is an example using the new notation.
def u : Pair := ⟨ 1,2 ⟩
def v : Pair := ⟨ 12,13 ⟩
#check u ≈ v
Finally, we register Pair
and eq
as a Setoid
, which is a set and an equivalence relation on the set. It is needed for the definition of the quotient space later.
instance pre_int_setoid : Setoid Pair :=
⟨ eq, eq_equiv ⟩
This exact process should be followed whenever defining a new equivalence class in Lean.
Quotients
The equivalence class of x
is defined to be the set of all pairs y
such that x≈y
. The set of all equivalence classes is called the quotient space, which we can form using Lean's Quotient
:
def Int := Quotient pre_int_setoid
We can then construct elements of Int
using Quotient.mk
.
def mk (w : Pair) : Int := Quotient.mk pre_int_setoid w
#check mk ⟨ 1, 2 ⟩ -- 1
#check mk ⟨ 2, 1 ⟩ -- -1
A key aspect of the quotient space is that equality is extended to elements of the quotient space. Thus, we can write:
#check mk ⟨ 1, 2 ⟩ = mk ⟨ 2, 3 ⟩
instead of using ≈
. As a result, we can us all the properties of equality we have become used to with basic types, such as definitional equality and substution.
We may now register a few more classes. The first defines zero, the second defines one, and the third defines a coercion from natural numbers to (non-negative) integers.
instance int_zero : Zero Int := ⟨ mk ⟨ 0,0 ⟩ ⟩
instance int_one : One Int := ⟨ mk ⟨ 1,0 ⟩ ⟩
instance int_of_nat {n: ℕ} :OfNat Int n := ⟨ mk ⟨ n, 0 ⟩ ⟩
#check (0:Int)
#check (1:Int)
#check (123:Int)
You may also encounter the notation ⟦⬝⟧ for equivalence classes. Since the notation does not know which equivalence relation you might be talking about, it is only useful in a context where the Type can be inferred.
-- Does not work:
#check_failure ⟦⟨1,2⟩⟧
-- Ok:
def my_int : Int := ⟦⟨1,2⟩⟧
Using Ints in Proofs
To prove theorems about negation we need some fundamental tools for proving properties of quotients:
-
Quotient.exists_rep
, which says∃ a, ⟦a⟧ = q
. This operator allows you to assert the existence of a representative of an equivalence class. Then, if you are trying to prove a result about the equivalence class, it amounts to proving it about the representative. -
Quotient.sound
, which saysa ≈ b → ⟦a⟧ = ⟦b⟧
. Applying this operator allows you to replace a goal involving proving two equivalence classes are equal, with one showing that representatives of the respective equivalence classes are equivalent under the associated Setoid relation. In other words, we unlift the equality back to the underlying space.
Using these two operations, we do a simple proof in which, for illustrative purposes, we write out each step. It is instructive to open this proof in VS Code and examine the proof state before and after each step.
example : ∀ x : Int, x = x := by
intro x
obtain ⟨ ⟨ a, b ⟩, hd ⟩ := Quotient.exists_rep x
rewrite[←hd]
apply Quotient.sound
simp only [pre_int_setoid,HasEquiv.Equiv,eq]
exact Nat.add_comm a b
The proof can be made much simpler in this example, because definitional equality is all you need in this case.
example : ∀ x : Int, x = x := by
intro x
rfl
However, the use of Quotient.exists_rep
and Quotient.sound
in the more complicated proofs is often needed, and the above pattern will be applicable in many situations.
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Lifting Operators
In this section we show how to lift an operation, such as negation or addition, from Pair
to Int
, and more generally from any type to a quotient on that type.
Lifting Negation
First, we define the meaning of negation for pairs, which is simply to switch the order of the pair's elements.
def pre_negate (x : Pair) : Pair := ⟨ x.q, x.p ⟩
We would like to lift the definition of negation to our new Int
type. This means defining negation of an entire equivalence class, which would only work if our pre_negate
function has the same result on all elements of an equivalence class, which fortunately it does! To capture this notion we define a respects theorem.
theorem pre_negate_respects (x y : Pair) :
x ≈ y → mk (pre_negate x) = mk (pre_negate y) := by
intro h
simp_all[pre_int_setoid,eq]
apply Quot.sound
simp[pre_int_setoid,eq,pre_negate,h]
With this theorem, we may use Lean's Quotient.lift
to define negate
on Int
.
def pre_negate' (x : Pair) : Int := mk (pre_negate x)
def negate (x : Int) : Int := Quotient.lift pre_negate' pre_negate_respects x
We may register our negation function wit the Neg
class, allowing us to use the -
notation for it, and to use any general theorems proved in Mathlib for types with negation.
instance int_negate : Neg Int := ⟨ negate ⟩
Now we can use negative integers.
#check -mk ⟨2,1⟩
#check -(1:Int)
Here is a simple theorem to test whether all of this is working.
theorem negate_negate { x : Int } : -(-x) = x := by
let ⟨ u, hu ⟩ := Quotient.exists_rep x
rw[←hu]
apply Quot.sound
simp[pre_int_setoid,pre_negate,eq]
linarith
Lifing Addition
Just as we lifted negation, we can also define and lift addition. For pairs, addition is just componentwise addition.
def pre_add (x y : Pair) : Pair := ⟨ x.p + y.p, x.q + y.q ⟩
To do the lift, we need another respects theorey. This time, the theorem assumes multiple equivalences.
theorem pre_add_respects (w x y z : Pair)
: w ≈ y → x ≈ z → mk (pre_add w x) = mk (pre_add y z) := by
intro h1 h2
apply Quot.sound
simp_all[pre_int_setoid,eq,pre_add]
linarith
With this theorem we can define addition on integers.
def pre_add' (x y : Pair) : Int := mk (pre_add x y)
def add (x y : Int) : Int := Quotient.lift₂ pre_add' pre_add_respects x y
And while we are at it, we can register addition with Mathlib's Add class so we can use +
.
instance int_add : Add Int := ⟨ add ⟩
Here is an example. It is remarkable to see that the entire edifice we have built can interact so easily with rfl
.
example : (1:Int) + 17 = 18 := rfl
A slightly more complicated example with addition and negation requires Quotient.sound
since we have not yet lifted any theorems from Pair
to Int
.
example : (1:Int) + (-2) = -1 := by
apply Quotient.sound
rfl
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Lifting Properties
In this section we show how to lift theorems from a type to a quotient on that type. Continuing with the Int
example, we lift all the properties required to show that Int
is an additive group. Then, using Lean's hierarchies, we formally instantiate Int
as an additive group using these theorems and show how we can then use all of Mathlib's group infrastructure on our new Int
type.
Lifting Theorems
There is no direct operator for lifting theorems, but doing so is straightforward. One typically defines a theorem on the base space and then uses it to prove the corresponding theorem on the quotient space. For example, here are several theorems defined on pairs.
theorem pre_add_com {x y: Pair} : eq (pre_add x y) (pre_add y x) := by
simp[eq,pre_add]
linarith
theorem pre_add_assoc {x y z: Pair}
: eq (pre_add (pre_add x y) z) (pre_add x (pre_add y z)) := by
simp[eq,pre_add]
linarith
theorem pre_zero_add (x : Pair) : eq (pre_add ⟨0,0⟩ x) x := by
simp[eq,pre_add]
linarith
theorem pre_inv_add_cancel (x : Pair) : eq (pre_add (pre_negate x) x) ⟨0,0⟩ := by
simp[eq,pre_negate,pre_add]
linarith
To lift these theorems to Int, we apply essentially the same pattern to each. We assert the existence of Pairs
that represent each of the intgers in the target theorem. We substitute those in for the integers. We use Quot.sound
to restate the goal in terms of pairs. Finally we use the underlying theorem. Here are several examples:
theorem add_comm (x y: Int) : x+y = y+x := by
have ⟨ u, hu ⟩ := Quotient.exists_rep x
have ⟨ v, hv ⟩ := Quotient.exists_rep y
rw[←hu,←hv]
apply Quot.sound
apply pre_add_com
theorem add_assoc (x y z: Int) : x+y+z = x+(y+z) := by
have ⟨ u, hu ⟩ := Quotient.exists_rep x
have ⟨ v, hv ⟩ := Quotient.exists_rep y
have ⟨ w, hw ⟩ := Quotient.exists_rep z
rw[←hu,←hv,←hw]
apply Quot.sound
apply pre_add_assoc
theorem zero_add (x : Int) : 0 + x = x := by
have ⟨ u, hu ⟩ := Quotient.exists_rep x
rw[←hu]
apply Quot.sound
apply pre_zero_add
theorem inv_add_cancel (x : Int) : -x + x = 0 := by
have ⟨ u, hu ⟩ := Quotient.exists_rep x
rw[←hu]
apply Quot.sound
apply pre_inv_add_cancel
Instantiating the Additive Group Structure on Int
The theorems above were chosen so that we could instantiate a everything we need to show that Int
forms an additive, commutative group.
instance int_add_comm_magma : AddCommMagma Int := ⟨ add_comm ⟩
instance int_add_semi : AddSemigroup Int := ⟨ add_assoc ⟩
instance int_add_comm_semi : AddCommSemigroup Int := ⟨ add_comm ⟩
instance int_group : AddGroup Int :=
AddGroup.ofLeftAxioms add_assoc zero_add inv_add_cancel
Now we get all the theorems and tactics about additive groups from Mathlib to apply to our Int
type! For example,
example (x: Int) : x-x = 0 := by
group
example (x y : Int) : x + y - y = x := by
exact add_sub_cancel_right x y
example (x y z : Int) : x+y+z = x+z+y := by
calc x+y+z
_ = x+(y+z) := by rw[add_assoc]
_ = x+(z+y) := by rw[add_comm y z]
_ = x+z+y := by rw[add_assoc]
Exercises
References
The construction here is defined in a number of Algebra textbooks. For an early example, see the Nicolas Bourbaki collective's book Algebra Part I, 1943. The English translation of that book from 1974 has the relevant material on page 20.
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
The Rational Numbers
The (pre) rational numbers are just pairs of an Int
and a Nat
. But we also have to keep track of whether the denomenator is non-zero. We do that be including in the structure definion the rationals a proof of that property. Thus, every rational number in Lean "knows" it is well-formed.
namespace LeanBook
structure PreRat where
intro ::
num : Int
den : Nat
dnz : den ≠ 0 := by decide -- works with constants
@[simp]
def eq (x y :PreRat) :=
x.num * y.den = y.num * x.den
Pre-rational admits many representations of the same number.
def p12 : PreRat := PreRat.intro 1 2
def p48 : PreRat := PreRat.intro 4 8
example : eq p12 p48 := rfl
Of course, Lean would define notation for all of this.
Defining the Rationals
One way to define the Rationals from the Pre-Rationals is to form the set of all elements equivalent to a given Pre-Rational. Then that set is
the rational.
For this to work, we have to show that the equality relation is an equivalence relation
. This means it needs to be:
- reflexive: eq x x
- symmetric: eq x y → eq y x
- transitive: eq x y ∧ eq y z → eq x z
We define the equivalence class of x to be
[x] = { y | x = y }
In this case, it is the set of all rationals that reduce to the same number.
The following are equivalent statements
eq x y [x] = [y] [x] ∩ [y] = ∅
Equality is Reflexive and Symmetric
theorem eq_refl {x : PreRat} : eq x x := by
rfl
theorem eq_symm {x y : PreRat} : eq x y → eq y x := by
intro h
simp[eq]
rw[eq] at h
apply Eq.symm
exact h
Transitivity is More Challenging.
We want to show
x = y and y = z → x = z
Or
p m m a p a ——— = ——— and ——— = ——— → ——— = ——— q n q n q b
But we can't use fractions.
To show that x = z, which is equivalent to pb = aq.
We have
pbn = pnb = mqb = qmb = qan = aqn
Thus pb = aq since n ≠ 0
Source: https://math.stackexchange.com/questions/1316069/how-do-i-show-that-the-equivalence-relation-defining-the-rational-numbers-is-tra
Proof of Transitivity
theorem eq_trans {x y z : PreRat}
: eq x y → eq y z → eq x z := by
intro h1 h2
let ⟨ p, q, _ ⟩ := x
let ⟨ m, n, nnz ⟩ := y
let ⟨ a, b, _ ⟩ := z
have h : p * b * n = a * q * n := by
calc p * b * n
_ = p * ( b * n ) := by rw[Int.mul_assoc]
_ = p * ( n * b ) := by simp[Int.mul_comm]
_ = p * n * b := by rw[Int.mul_assoc]
_ = m * q * b := by rw[h1]
_ = q * m * b := by simp[Int.mul_comm]
_ = q * (m * b) := by simp[Int.mul_assoc]
_ = q * (a * n) := by rw[h2]
_ = q * a * n := by simp[Int.mul_assoc]
_ = a * q * n := by simp[Int.mul_comm]
simp at h
apply Or.elim h
. exact id
. intro h
exact False.elim (nnz h)
Building the Rationals
inductive Rat where
| of_pre_rat : PreRat → Rat
open Rat
def P12 := of_pre_rat p12
def P48 := of_pre_rat p48
Lifting Equality to the Rationals
@[simp]
def LiftRel (r : PreRat → PreRat → Prop) (x y : Rat) : Prop :=
match x, y with
| of_pre_rat a, of_pre_rat b => r a b
@[simp]
def req := LiftRel eq
example : req P12 P48 := by
simp[P12,P48,p12,p48]
Lifting Funtions
@[simp]
def pre_negate (x : PreRat) : PreRat := ⟨ -x.num, x.den, x.dnz ⟩
def Respects (f : PreRat → PreRat) := ∀ x y : PreRat, eq x y → eq (f x) (f y)
theorem negate_respects : Respects pre_negate := by
intro h
simp_all[pre_negate,eq]
@[simp]
def LiftFun (f : PreRat → PreRat) (x : Rat) : Rat := match x with
| of_pre_rat a => of_pre_rat (f a)
@[simp]
def negate := LiftFun pre_negate
example : negate (negate P12) = P12 := by
simp[P12]
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Real Numbers
Having built ℕ inductively, ℤ from ℕ, and ℚ from ℕ and ℤ, we now turn to the real numbers ℝ. The key distinction between the ℚ and ℝ is that ℝ has the least upper bound property: every non-empty set of reals that is bounded from above has a least upper bound. The rationals ℚ do not have this property. For example, the set
{ q ∈ ℚ | q² < 2 }
has no least upper bound in ℚ, although in ℝ the least upper bound is √2
(for a Lean proof that the square root of 2 is no rational, see Chapter 5 of MIL.)
The usual way to construct ℝ, and the one taken by Mathlib, is to define Cauchy Sequences
over ℚ
that converge to irrational values. For example, the sequence
4/1
4/1 - 4/3
4/1 - 4/3 + 4/5
4/1 - 4/3 + 4/5 - 4/7
4/1 - 4/3 + 4/5 - 4/7 + 4/9
Converges to π
.
In this chapter, mainly as an exercise in formalization, we instead construct ℝ from Dedekind Cuts in which every real number is equated to the set of rational numbers less than it. We roughly follow the decription of this approach presented in Rudin's book Principles of Mathematical Analysis (which also, by the way, describes the Cauchy Sequence approach.)
Axioms of the Reals
To show that a set R is equivalent to the real numbers, we define the following relations and operations:
- Ordering relations: <, ≤, > and ≥
- Addition and subtraction: +, -
- Multiplication: *
- Division: /
and we require the following:
- R is totally ordered: ∀ x y ∈ R, x < y ∨ y < x ∨ x=y
- R is a field
- Addition properties
- Addition is commutative: x+y=y+x
- Addition is associative: x+(y+z) = (x+y)+z
- There is an additive identity 0 and x+0 = 0+x = x.
- Each element x has an additive inverse -x such that x+(-x) = 0.
- Multiplication properties
- Multiplication is commutative: xy=yx
- Multiplication is associative: x*(yz) = (xy)*z
- There is a multiplicative identity 1 and x1 = 1x = x.
- Each element x has a multiplicative inverse x⁻¹ such that xx⁻¹ = x⁻¹x = 1.
- Addition properties
- R has the least upper bound property
Mathlib defines classes for all of these properties. Thus, as we prove them in Lean, we will register intances of these classes and more so that our construction works with all of the theorems and tactics that Mathlib provides for notation, ordering, groups, rings, and fields.
TODO: List the relevant Mathlib classes or insert a tree diagram of them.
References
A nice description of the Cauchy Completion: https://mathweb.ucsd.edu/~tkemp/140A/Construction.of.R.pdf
Rudin, W.: Principles of Mathematical Analysis. Third Edition. International Series in Pure and Applied Mathematics. McGraw-Hill Book Co., New York – Aukland – Dusseldorf, 1976.
A real analysis book in which ℝ is constructed from decimal expansions of the form f : ℕ → ℤ with f(0) being the integer part and f(n) ∈ {0, ..., 9} for n ≥ 1. Davidson and Donsig
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
The Dedekind Reals
Dedekind's representation of a real number r
is as a pair (A,B)
where A ⊆ ℚ
is the set of all rational numbers less than r
and B = ℚ \ A
. The idea is that A
approximates r
from below and B
approximates r
from above. In the case that r ∈ ℚ
, then A = (∞,r)
and B = [r,∞)
. Since A
completely determines the cut, we work mainly with it, only occasionally referring to B
.
That this definition satisfies the properties of the real numbers needs to be proved, which is what this chapter is about. Specifically, we will prove that
- The set of cuts is totally ordered
- The set of cuts form a field
- Every bounded, non-empty set of cuts has a least upper bound
The last property distinguishes the real numbers from the rationals.
A standard reference for Dedekind cuts is Rudin's Principles of Mathematics. In the 3rd edition, cuts are defined on pages 17-21.
Definition
First, we define a structure to capture the precise definition of a cut A ⊆ ℚ
. We require that A is nonempty, that it is not ℚ, that it is downward closed, and that is an open interval.
@[ext]
structure DCut where
A : Set ℚ
ne : ∃ q, q ∈ A -- not empty
nf : ∃ q, q ∉ A -- not ℚ
dc : ∀ x y, x ≤ y ∧ y ∈ A → x ∈ A -- downward closed
op : ∀ x ∈ A, ∃ y ∈ A, x < y -- open
open DCut
def DCut.B (c : DCut) : Set ℚ := Set.univ \ c.A
theorem not_in_a_in_b {c :DCut} {q : ℚ} : q ∉ c.A → q ∈ c.B := by simp[B]
theorem ub_to_notin {y:ℚ} {A : Set ℚ}
: (∀ x ∈ A, x < y) → y ∉ A := by
intro h hy
have := h y hy
simp_all
The open property can be used extended.
theorem op2 {a : DCut} (q : ℚ) (hq : q ∈ a.A)
: ∃ x, ∃ y, x ∈ a.A ∧ y ∈ a.A ∧ q < x ∧ x < y := by
have ⟨s, ⟨ hs1, hs2 ⟩ ⟩ := a.op q hq
have ⟨t, ⟨ ht1, ht2 ⟩ ⟩ := a.op s hs1
use s, t
Making Rationals into Reals
All rational numbers are also real numbers via the map that identifies a rational q
with the interval (∞,q)
of all rationals less than q
. We call this set odown q
, where odown
is meant to abbreviate open, downward closed
.
def odown (q : ℚ) : Set ℚ := { y | y < q }
To prove that odown q
is a Dedekind cut requires we show it is nonempty, not ℚ
itself, downward closed, and open. For the first two theorems we use use the facts that q-1 ∈ (∞,q)
and q+1 ∉ (∞,q)
.
theorem odown_ne {q : ℚ} : ∃ x, x ∈ odown q := by
use q-1
simp[odown]
theorem odown_nf {q : ℚ} : ∃ x, x ∉ odown q := by
use q+1
simp[odown]
That odown
q is downward closed follows from the definitions.
theorem odown_dc {q : ℚ} : ∀ x y, x ≤ y ∧ y ∈ odown q → x ∈ odown q := by
intro x y ⟨ hx, hy ⟩
simp_all[odown]
linarith
To prove odown q
is open, we are given x ∈ odown
and need to supply y ∈ odown q
with x < y
. Since q
is the least upper bound of odown q
, we choose (x+q)/2
.
theorem odown_op {q : ℚ} : ∀ x ∈ odown q, ∃ y ∈ odown q, x < y:= by
intro x hx
use (x+q)/2
simp_all[odown]
apply And.intro
repeat linarith
With these theorems we define the map ofRat : ℚ → DCut
that embeds the rationals into the Dedekind cuts.
def ofRat (q : ℚ) : DCut :=
⟨ odown q, odown_ne, odown_nf, odown_dc, odown_op ⟩
instance rat_cast_inst : RatCast DCut := ⟨ λ x => ofRat x ⟩
instance nat_cast_inst : NatCast DCut := ⟨ λ x => ofRat x ⟩
instance int_cast_inst : IntCast DCut := ⟨ λ x => ofRat x ⟩
With this map we can define 0 and 1, as well as a couple of helper theorems we will later.
instance zero_inst : Zero DCut := ⟨ ofRat 0 ⟩
instance one_inst : One DCut := ⟨ ofRat 1 ⟩
instance inhabited_inst : Inhabited DCut := ⟨ 0 ⟩
theorem zero_rw : A 0 = odown 0 := by rfl
theorem one_rw : A 1 = odown 1 := by rfl
theorem zero_ne_one : (0:DCut) ≠ 1 := by
intro h
simp[DCut.ext_iff,zero_rw,one_rw,odown,Set.ext_iff] at h
have h0 := h (1/2)
have h1 : (1:ℚ)/2 < 1 := by linarith
have h2 : ¬(1:ℚ)/2 < 0 := by linarith
exact h2 (h0.mpr h1)
instance non_triv_inst : Nontrivial DCut := ⟨ ⟨ 0, 1, zero_ne_one ⟩ ⟩
Simple Properties of Cuts
Here we define some simple properties that wil make subsequent proofs less cumbersome. The first says for x in A
and y in B
, that x < y
.
theorem b_gt_a {c : DCut} {x y : ℚ} : x ∈ c.A → y ∈ c.B → x < y := by
intro hx hy
simp[B] at hy
by_contra h
exact hy (c.dc y x ⟨ Rat.not_lt.mp h, hx ⟩)
An alternative for of this same theorem, in which B
is characterized as ℚ \ A
is also useful.
theorem a_lt_b {c : DCut} {x y : ℚ }: x ∈ c.A → y ∉ c.A → x < y := by
intro hx hy
exact b_gt_a hx (not_in_a_in_b hy)
Since A
is downward closed, one can easily show B
is upward closed.
theorem b_up_closed {c : DCut} {a b: ℚ} : a ∉ c.A → a ≤ b → b ∉ c.A := by
intro h1 h2 h3
exact h1 (c.dc a b ⟨ h2, h3 ⟩)
Ordering
Next we show that cuts are totally ordered by the subset relation. First, we define and instantiate the less than and less than or equal relations on cuts.
instance lt_inst : LT DCut := ⟨ λ x y => x ≠ y ∧ x.A ⊆ y.A ⟩
instance le_inst : LE DCut := ⟨ λ x y => x.A ⊆ y.A ⟩
To check these definitions make sense, we verify them with rational numbers.
example {x y : ℚ} : x ≤ y → (ofRat x) ≤ (ofRat y) := by
intro h q hq
exact gt_of_ge_of_gt h hq
It is useful to be able to rewrite the less than relation <
in terms of inequality and ≤
, and to rewrite ≤
in terms of equality and <
.
theorem DCut.lt_of_le {x y: DCut} : x < y ↔ x ≠ y ∧ x ≤ y := by
exact Eq.to_iff rfl
theorem DCut.le_of_lt {x y: DCut} : x ≤ y ↔ x = y ∨ x < y := by
simp_all[le_inst,lt_inst]
constructor
. intro h
simp[h]
exact Classical.em (x=y)
. intro h
cases h with
| inl h1 => exact Eq.subset (congrArg A h1)
| inr h1 => exact h1.right
We can easily prove that cuts form a partial order, which allows us to regiest DCut with Mathlib's PartialOrder class.
theorem refl {a: DCut} : a ≤ a := by
intro q hq
exact hq
theorem anti_symm {a b: DCut} : a ≤ b → b ≤ a → a = b := by
intro hab hba
ext q
constructor
. intro hq
exact hab (hba (hab hq))
. intro hq
exact hba (hab (hba hq))
theorem trans {a b c: DCut} : a ≤ b → b ≤ c → a ≤ c := by
intro hab hbc q hq
exact hbc (hab hq)
theorem lt_iff_le_not_le {a b : DCut} : a < b ↔ a ≤ b ∧ ¬b ≤ a := by
constructor
. intro h
rw[lt_of_le] at h
have ⟨ h1, h2 ⟩ := h
constructor
. exact h.right
. intro h3
exact h1 (anti_symm h.right h3)
. intro ⟨ h1, h2 ⟩
rw[le_of_lt] at h1
apply Or.elim h1
. intro h
rw[h] at h2
exact False.elim (h2 refl)
. intro h
exact h
instance pre_order_inst : Preorder DCut :=
⟨ @refl, @trans, @lt_iff_le_not_le ⟩
instance poset_inst : PartialOrder DCut :=
⟨ @anti_symm ⟩
Next we prove that cuts form a total order, and instantiate this fact with the TotalOrder class from Mathlib.
theorem total {x y : DCut} : x ≤ y ∨ y ≤ x := by
by_cases h : x ≤ y
. exact Or.inl h
. apply Or.inr
simp_all[le_inst]
intro b hb
rw[Set.subset_def] at h
simp at h
obtain ⟨ a, ⟨ ha1, ha2 ⟩ ⟩ := h
exact x.dc b a ⟨ le_of_lt (a_lt_b hb ha2), ha1 ⟩
instance total_inst : IsTotal DCut (· ≤ ·) := ⟨ @total ⟩
The total order property allows crisply define positive and negative numbers.
def isPos (x : DCut) : Prop := 0 < x
def isNeg (x : DCut) : Prop := x < 0
We can also use the total order property to prove that DCut
is Trichotomous, that is, that for all x
and y
, either x ≤ y
, y ≤ x
, or x=y
.
theorem trichotomy (x y: DCut) : x ≤ y ∨ x = y ∨ y ≤ x := by
apply Or.elim (@total x y)
. intro h
exact Or.symm (Or.inr h)
. intro h
exact Or.inr (Or.inr h)
theorem trichotomy_lt (x y: DCut) : x < y ∨ x = y ∨ y < x := by
have := trichotomy x y
simp[le_of_lt] at this
aesop
instance trichotomous_inst : IsTrichotomous DCut (· ≤ ·) := ⟨ trichotomy ⟩
instance trichotomous_inst' : IsTrichotomous DCut (· < ·) := ⟨ trichotomy_lt ⟩
Theorems About Zero and One
theorem zero_in_pos {a : DCut} (ha : 0 < a) : 0 ∈ a.A := by
obtain ⟨ h1, h2 ⟩ := ha
simp at h1
rw[DCut.ext_iff] at h1
have h21 := Set.ssubset_iff_subset_ne.mpr ⟨h2, h1⟩
have ⟨ x, ⟨ hx1, hx2 ⟩ ⟩ := (Set.ssubset_iff_of_subset h2).mp h21
simp[zero_rw,odown] at hx2
exact a.dc 0 x ⟨ hx2, hx1 ⟩
theorem pos_has_zero {a : DCut} : 0 < a ↔ 0 ∈ a.A := by
constructor
. intro h
exact zero_in_pos h
. simp[lt_inst,DCut.ext_iff]
intro h
constructor
. intro h1
rw[←h1] at h
simp[zero_rw,odown] at h
. intro q hq
simp[zero_rw,odown] at hq
apply a.dc q 0
exact ⟨ by exact _root_.le_of_lt hq, h ⟩
theorem non_neg_in_pos {a : DCut} (ha : 0 < a) : ∃ x ∈ a.A, 0 < x := by
have h0 := zero_in_pos ha
exact a.op 0 h0
theorem zero_notin_neg {a : DCut} (ha : a < 0) : 0 ∉ a.A := by
intro h
simp[lt_inst] at ha
have ⟨ h1, h2 ⟩ := ha
have : 0 ∈ A 0 := h2 h
simp[zero_rw,odown] at this
@[simp]
theorem zero_lt_one : (0:DCut) < 1 := by
simp[lt_inst]
apply And.intro
. intro h
simp[DCut.ext_iff,zero_rw,one_rw,odown,Set.ext_iff] at h
have := h 0
simp_all
. intro q hq
simp_all[zero_rw,one_rw,odown]
linarith
@[simp]
theorem zero_le_one : (0:DCut) ≤ 1 := by
simp[le_inst]
intro q hq
simp_all[zero_rw,one_rw,odown]
linarith
theorem not_gt_to_le {a : DCut} : ¬ 0 < a ↔ a ≤ 0 := by
constructor
. have := trichotomy_lt 0 a
apply Or.elim this
. intro h1 h2
simp_all
. intro h1 h2
simp_all
apply le_of_lt.mpr
rw[Eq.comm]
exact h1
. intro h1 h2
apply le_of_lt.mp at h1
simp_all[DCut.ext_iff,lt_inst]
have ⟨ h3, h4 ⟩ := h2
simp_all
apply Or.elim h1
. intro h
exact h3 (id (Eq.symm h))
. intro ⟨ h5, h6 ⟩
have : A 0 = a.A := by exact Set.Subset.antisymm h4 h6
exact h3 this
theorem has_ub (a : DCut) : ∃ x, x ∉ a.A ∧ ∀ y ∈ a.A, y < x := by
obtain ⟨ x, hx ⟩ := a.nf
use! x, hx
intro y hy
exact a_lt_b hy hx
theorem in_down {p q:ℚ} (h : p < q) : p ∈ odown q := by
simp[odown]
exact h
theorem in_zero {q:ℚ} (h: q<0) : q ∈ A 0 := by
simp[zero_rw]
exact in_down h
theorem in_one {q:ℚ} (h: q<1) : q ∈ A 1 := by
simp[one_rw]
exact in_down h
Supporting Reasoning by Cases
Later, in the chapter on multiplication, it will be useful to reason about combinations of non-negative and negative cuts. With one cut a
, there are two possibilities: 0 ≤ a
and a < 0
. For two cuts there are four possibilities, and for three cuts, there are eight possibilities.
The following two definitions list all possible cases for two and three cuts respectively.
def two_ineq_list (a b : DCut) :=
(0 ≤ a ∧ 0 ≤ b) ∨
(a < 0 ∧ 0 ≤ b) ∨
(0 ≤ a ∧ b < 0) ∨
(a < 0 ∧ b < 0)
def two_ineq_nn_list (a b: DCut) :=
(0 < a ∧ 0 < b) ∨ a = 0 ∨ b = 0
def three_ineq_list (a b c : DCut) :=
(0 ≤ a ∧ 0 ≤ b ∧ 0 ≤ c) ∨
(a < 0 ∧ 0 ≤ b ∧ 0 ≤ c) ∨
(0 ≤ a ∧ b < 0 ∧ 0 ≤ c) ∨
(0 ≤ a ∧ 0 ≤ b ∧ c < 0) ∨
(a < 0 ∧ b < 0 ∧ 0 ≤ c) ∨
(a < 0 ∧ 0 ≤ b ∧ c < 0) ∨
(0 ≤ a ∧ b < 0 ∧ c < 0) ∨
(a < 0 ∧ b < 0 ∧ c < 0)
def three_ineq_nn_list (a b c : DCut) :=
(0 < a ∧ 0 < b ∧ 0 < c) ∨ a = 0 ∨ b = 0 ∨ c = 0
Next we show that these statements are tautologies. The goal is to be able to use the definitions in tactic mode, as in:
rcases @two_ineqs a b with ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩
repeat
simp
We start with a theorem that can be used to rewrite x<0
as ¬0≤x
.
theorem neg_t {x : DCut} : x < 0 ↔ ¬0 ≤ x := by
have := trichotomy_lt 0 x
simp_all[le_of_lt]
constructor
. intro h
exact ⟨ ne_of_gt h, not_lt_of_gt h ⟩
. tauto
Then the proofs are straightforward. To see how these are used later, see the proofs of commutativity and associativity of multiplication.
theorem lt_imp_le {x y : DCut} : x < y → x ≤ y := by simp[lt_of_le]
theorem two_ineqs (a b : DCut) : two_ineq_list a b := by
simp[two_ineq_list,neg_t]
tauto
theorem three_ineqs (a b c : DCut) : three_ineq_list a b c := by
simp[three_ineq_list,neg_t]
tauto
theorem two_nn_ineqs {a b : DCut} (ha : 0 ≤ a) (hb : 0 ≤ b)
: two_ineq_nn_list a b := by
simp[two_ineq_nn_list,neg_t]
simp[le_of_lt] at ha hb
tauto
theorem three_nn_ineqs {a b c : DCut} (ha : 0 ≤ a) (hb : 0 ≤ b) (hc : 0 ≤ c)
: three_ineq_nn_list a b c := by
simp[three_ineq_nn_list,neg_t]
simp[le_of_lt] at ha hb hc
tauto
Exercise: Show that ofRat
is indeed an order embedding, that is x ≤ y → ofRat x ≤ ofRat y
for all rational numbers x
and y
.
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Addition
def presum (a b : DCut) := { z | ∃ x ∈ a.A, ∃ y ∈ b.A, x+y=z }
theorem presum_ne {a b : DCut} : ∃ q, q ∈ presum a b := by
obtain ⟨ x, hx ⟩ := a.ne
obtain ⟨ y, hy ⟩ := b.ne
exact ⟨ x+y, ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, by linarith ⟩ ⟩ ⟩ ⟩ ⟩
theorem presum_nf {a b : DCut} : ∃ q, q ∉ presum a b := by
obtain ⟨ x, hx ⟩ := a.nf
obtain ⟨ y, hy ⟩ := b.nf
use x+y
intro h
obtain ⟨ s, ⟨ hs, ⟨ t, ⟨ ht, hst ⟩ ⟩ ⟩ ⟩ := h
have hs' := b_gt_a hs (not_in_a_in_b hx)
have ht' := b_gt_a ht (not_in_a_in_b hy)
linarith
theorem presum_op {a b : DCut}
: ∀ x ∈ presum a b, ∃ y ∈ presum a b, x < y := by
intro c hc
simp_all[presum]
obtain ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, h ⟩ ⟩ ⟩ ⟩ := hc
have hao := a.op
have hbo := b.op
obtain ⟨ x', hx', hxx' ⟩ := hao x hx
obtain ⟨ y', hy', hyy' ⟩ := hbo y hy
use x'
apply And.intro
. exact hx'
. use y'
apply And.intro
. exact hy'
. linarith
theorem presum_dc {a b : DCut }
: ∀ (x y : ℚ), x ≤ y ∧ y ∈ presum a b → x ∈ presum a b := by
intro s t ⟨ h1, h2 ⟩
simp_all[presum]
obtain ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, h ⟩ ⟩ ⟩ ⟩ := h2
have hyts : y - (t - s) ∈ b.A := by
have h3 : 0 ≤ t-s := by linarith
have h4 : y - (t-s) ≤ y := by linarith
exact b.dc (y-(t-s)) y ⟨h4,hy⟩
exact ⟨ x, ⟨ hx, ⟨ y - (t-s), ⟨ hyts, by linarith ⟩ ⟩ ⟩ ⟩
def sum (a b : DCut) : DCut :=
⟨ presum a b, presum_ne, presum_nf, presum_dc, presum_op ⟩
instance hadd_inst : HAdd DCut DCut DCut:= ⟨ sum ⟩
instance add_inst : Add DCut := ⟨ sum ⟩
theorem add_rats {x y: ℚ} : ofRat (x+y) = ofRat x + ofRat y := by
simp[ofRat,hadd_inst,sum,presum,odown]
ext q
constructor
. intro hq
let ε := x+y - q
have hε : q = x+y-ε := by
simp[ε]
simp_all
exact ⟨ x-ε/2, ⟨ by linarith, ⟨ y-ε/2, ⟨ by linarith, by linarith ⟩ ⟩ ⟩ ⟩
. intro ⟨ a, ⟨ ha, ⟨ b, ⟨ hb1, hb2 ⟩ ⟩ ⟩ ⟩
simp_all
linarith
The Associative Property Of Addition
theorem sum_assoc {a b c : DCut} : (a+b)+c = a + (b+c) := by
simp[hadd_inst,sum]
ext q
constructor
. intro hq
simp_all[presum]
obtain ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, ⟨ z, ⟨ hz, hsum ⟩ ⟩ ⟩ ⟩ ⟩ ⟩ := hq
exact ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, ⟨ z, ⟨ hz, by linarith ⟩ ⟩ ⟩ ⟩ ⟩ ⟩
. intro hq
simp_all[presum]
obtain ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, ⟨ z, ⟨ hz, hsum ⟩ ⟩ ⟩ ⟩ ⟩ ⟩ := hq
exact ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, ⟨ z, ⟨ hz, by linarith ⟩ ⟩ ⟩ ⟩ ⟩ ⟩
instance addsemigroup_inst : AddSemigroup DCut := ⟨ @sum_assoc ⟩
Commutativity of Addition
theorem sum_comm {a b : DCut} : a + b = b + a := by
simp[hadd_inst,sum]
ext q
constructor
repeat
. intro ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, h ⟩ ⟩ ⟩ ⟩
exact ⟨ y, ⟨ hy, ⟨ x, ⟨ hx, by linarith ⟩ ⟩ ⟩ ⟩
instance addcommsemigroup_inst : AddCommSemigroup DCut := ⟨ @sum_comm ⟩
Adding Zero
theorem sum_zero_left {a : DCut} : 0 + a = a := by
ext c
simp[hadd_inst,sum,zero_inst]
constructor
. intro ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, h ⟩ ⟩ ⟩ ⟩
have : x < 0 := hx
apply a.dc c y
apply And.intro
. linarith
. exact hy
. intro h
obtain ⟨ x, ⟨ hx1, hx2 ⟩ ⟩ := a.op c h
have h1 : c-x < 0 := by linarith
exact ⟨ c-x, ⟨ h1, ⟨ x, ⟨ hx1, by linarith ⟩ ⟩ ⟩ ⟩
theorem sum_zero_right {a : DCut} : a + 0 = a := by
rw[sum_comm,sum_zero_left]
instance add_zero_inst : AddZeroClass DCut :=
⟨ @sum_zero_left, @sum_zero_right ⟩
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Negation
def preneg (c : DCut) : Set ℚ := { x | ∃ a < 0, ∃ b ∉ c.A, x = a-b }
theorem preneg_rat {p : ℚ} : preneg (ofRat p) = (ofRat (-p)).A := by
simp[preneg,ofRat,odown]
ext q
constructor
. simp_all
intro a ha x hx hq
linarith
. simp_all
intro hq
exact ⟨ q+p, ⟨ by linarith, ⟨ p, ⟨ by linarith, by linarith ⟩ ⟩ ⟩ ⟩
theorem predeg_ne {c : DCut} : ∃ q, q ∈ preneg c := by
simp[preneg]
have ⟨ q, hq ⟩ := c.nf
use -q-2
have h1 : q + 1 ∉ c.A := by
apply b_up_closed hq
linarith
exact ⟨ -1, ⟨ by linarith, ⟨ q+1, ⟨ h1, by linarith ⟩ ⟩ ⟩ ⟩
theorem predeg_nf {c : DCut} : ∃ q, q ∉ preneg c := by
simp[preneg]
have ⟨ q, hq ⟩ := c.ne
use -q
intro x hx y hy h
have h2 : y ≤ q := by linarith
have h3 : q ∉ c.A := by
intro h1
exact b_up_closed hy h2 hq
exact h3 hq
theorem predeg_dc {c : DCut}
: ∀ (x y : ℚ), x ≤ y ∧ y ∈ preneg c → x ∈ preneg c := by
intro x y ⟨ hxy, ⟨ a, ⟨ ha, ⟨ b, ⟨ hb, h ⟩ ⟩ ⟩ ⟩ ⟩
exact ⟨ a - (y-x), ⟨ by linarith, ⟨ b, ⟨ hb, by linarith ⟩ ⟩ ⟩ ⟩
theorem predeg_op {c : DCut}
: ∀ x ∈ preneg c, ∃ y ∈ preneg c, x < y := by
simp[preneg]
intro q x hx y hy hxy
have := c.op
use q-x/2
apply And.intro
. simp[hxy]
exact ⟨ x/2, ⟨ by linarith, ⟨ y, ⟨ hy, by linarith ⟩ ⟩ ⟩ ⟩
. linarith
def neg (c : DCut) : DCut :=
⟨ preneg c, predeg_ne, predeg_nf, predeg_dc, predeg_op ⟩
instance neg_inst : Neg DCut := ⟨ neg ⟩
theorem neg_rat {p : ℚ} : -ofRat p = ofRat (-p) := by
simp[neg_inst,neg]
apply DCut.ext
simp
rw[preneg_rat]
Subtraction
def sub (a b : DCut) : DCut := a + (-b)
instance hsub_inst : HSub DCut DCut DCut := ⟨ sub ⟩
instance sub_inst : Sub DCut := ⟨ sub ⟩
theorem add_neg (a b : DCut) : a + -b = a - b := by
simp[hsub_inst,sub]
Let's check this definition works for the simple example 1-1=0
. The forward direction is easy. For the reverse direction, we need to choose x and y so that
x < 1
y < -1
x + y = q
Since q < 0, we know q-1 < -1. For y, we take the point halfway between q-1 and -1, which is y = (q-2)/2
.
example : ofRat 1 - ofRat 1 = ofRat 0 := by
simp[hsub_inst,sub,neg_rat]
ext q
simp[hadd_inst,sum,ofRat,presum,odown]
constructor
. intro ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, h ⟩ ⟩ ⟩ ⟩
linarith
. intro hq
exact ⟨ (q+2)/2, ⟨ by linarith, ⟨ (q-2)/2, ⟨ by linarith, by linarith ⟩ ⟩ ⟩ ⟩
More challenging is to show for any cut c
that c-c=0
. We are given q<0
. Since c.A
is not ℚ
, we can choose an element t ∉ c.A
. We then want to find a ∈ -c.A
and b ∈ c.A
so that a+b=q
. Using the Archimedean property of the rational numbers, we can find n
such that t + (n+1)q ∈ c.A
and t + nq ∉ c.A
.
These values do not work directly because of the edge case where c
is a principal cut (representing a rational number). For example, the situation could look like the diagram below.
-c.A b c.A -a
-t - nq q 0 t + (n+1)q t + nq
←———————+———————)————————+———————+———————+————————)—————+————————————→
-2 -1 4-3=1 2 4-2=2
But because c.A
is open, we can find z
greater than t + (n+1)q
and which is still in c.A.
-c.A c.A
q 0 t + (n+1)q t + nq
←———————)———————————+——————————+————————+————————+———————)————————+————————————→
z
And then take ε = z - (t + (n+1)q) to get the following situation, where a
is completly inside -c.A
.
a -c.A b c.A
←———————+———————)———————+———————+————————+———————————)———————+————————————→
-t - nq - ε q 0 t + (n+1)q + ε t + nq + ε
This proof outline takes some work to get done in Lean. First we need a result showing that given t ∉ c.A
we can find an n
so that t + n s ∈ c.A
.
theorem cut_element (c : DCut) (s t : ℚ) (hs : s < 0)
: ∃ n : ℕ, t + n * s ∈ c.A := by
obtain ⟨q, hq⟩ := c.ne
let n := ⌈(q-t)/s⌉₊
use n
have hdiv : (q-t)/s ≤ n := Nat.le_ceil ((q - t) / s)
have hcalc : t + n * s ≤ q := by
have : (q-t) ≥ n * s := (div_le_iff_of_neg hs).mp hdiv
simp_all
linarith
exact c.dc _ q ⟨hcalc, hq⟩
We then define the set of all n
such that t + ns ∈ c.A. For subsequent steps to work, we need to instantiate this set as decidable. This can be done using Classical logic, in which propositions are considered decidable by default.
def Svals (c : DCut) (s t : ℚ) : Set ℕ := {n : ℕ | t + n * s ∈ c.A}
noncomputable
instance s_dec {c : DCut} {s t : ℚ} : DecidablePred (· ∈ Svals c s t) := by
intro n
apply Classical.propDecidable
With Svals
decidiable and nonempty, we can use Nat.find
to show it has a minimal element. Note that Nat.find
uses the Axiom of Choice.
theorem min_element (S : Set ℕ) [DecidablePred (· ∈ S)] (h : ∃ x, x ∈ S)
: ∃ m, m ∈ S ∧ (∀ k < m, k ∉ S) := by
have hsne : S.Nonempty := h
let m := Nat.find hsne
have hm : m ∈ S := Nat.find_spec hsne
have hm_min : ∀ k < m, k ∉ S := λ k => Nat.find_min hsne
exact ⟨ m, hm, hm_min ⟩
We use the minimal element to find n
so that t + (n+1)q ∈ c.A
and t + nq ∉ c.A
. as desired.
theorem archimedean {c : DCut} {s t : ℚ} (ht : t ∉ c.A) (hs : s < 0)
: ∃ n : ℤ, t+n*s ∉ c.A ∧ t+(n+1)*s ∈ c.A := by
let S := Svals c s t
have ⟨ m, hm, hm_min ⟩ := min_element S (cut_element c s t hs)
by_cases h : m = 0
· simp [h, S, Svals] at hm
contradiction
· use m - 1
have hm' : m > 0 := Nat.zero_lt_of_ne_zero h
apply And.intro
· have := hm_min (m-1) (Nat.sub_one_lt_of_lt hm')
simp_all[S,Svals]
· simp
assumption
Finally, we prove the desired theorem.
theorem neg_add_cancel_right {c : DCut} : c - c = 0 := by
ext q
constructor
. simp[hsub_inst,neg_inst,neg,sub,hadd_inst,sum,preneg]
intro ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, hxy ⟩ ⟩ ⟩ ⟩
obtain ⟨ a, ⟨ ha, ⟨ b, ⟨ hb, hab ⟩ ⟩ ⟩ ⟩ := hy
have h1 : q ∈ A 0 ↔ q < 0 := Set.mem_def
simp[h1]
have h2 := a_lt_b hx hb
linarith
. intro hq
have hq : q < 0 := hq
obtain ⟨ t, ht ⟩ := c.nf
obtain ⟨ n, ⟨ hn1, hn2 ⟩ ⟩ := archimedean ht hq
let b' := t + (n+1)*q
let a' := -n*q-t
obtain ⟨ z, ⟨ hz, hbz ⟩ ⟩ := c.op b' hn2
let ε := z - b'
have hε : 0 < ε := by simp[ε]; linarith
let b := z
let a := -n*q-t-ε
have hab : z+a = q := by
simp[a,a',b,b',ε]
linarith
have ha : a ∈ (-c).A := by
use -ε
apply And.intro
. linarith
. use -a-ε
apply And.intro
. simp[a]
exact hn1
. linarith
exact ⟨ z, ⟨ hz, ⟨ a, ⟨ ha, hab ⟩ ⟩ ⟩ ⟩
And by commutativity:
theorem neg_add_cancel_left {c : DCut} : -c + c = 0 := by
rw[sum_comm,add_neg,neg_add_cancel_right]
Additive Group Structure
instance add_group_inst : AddGroup DCut :=
AddGroup.ofLeftAxioms add_assoc @sum_zero_left @neg_add_cancel_left
instance add_comm_group_inst : AddCommGroup DCut := ⟨ @sum_comm ⟩
example (x y z : DCut) : x - y + z = z + x - y := by abel
instance add_monoid_wo_inst : AddMonoidWithOne DCut := ⟨
by
ext q
exact ⟨ id, id ⟩,
by
intro n
ext q
constructor
repeat
. intro hq
simp_all[add_rats,nat_cast_inst]
exact hq
⟩
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
The Maximum Operator
theorem neg_gt_zero {a : DCut} (ha : a < 0) : 0 < -a := by
simp[lt_inst]
apply And.intro
. intro h
obtain ⟨ h1, h2 ⟩ := ha
exact h1 h
. intro q hq
simp[zero_rw,odown] at hq
simp[neg_inst,neg,preneg]
obtain ⟨ b, hb ⟩ := a.nf
exact ⟨ q, ⟨ hq, ⟨ 0, ⟨ zero_notin_neg ha, Eq.symm (sub_zero q) ⟩ ⟩ ⟩ ⟩
theorem maximum_ne {a b : DCut} : ∃ q, q ∈ a.A ∪ b.A := by
obtain ⟨ x, hx ⟩ := a.ne
use x
exact Set.mem_union_left b.A hx
theorem maximum_nf {a b : DCut} : ∃ q, q ∉ a.A ∪ b.A := by
obtain ⟨ x, hx ⟩ := a.nf
obtain ⟨ y, hy ⟩ := b.nf
apply Or.elim (trichotomy a b)
. intro h
simp_all
use y
apply And.intro
. intro h1
exact hy (h h1)
. exact hy
. intro h
apply Or.elim h
. intro h1
simp[h1]
use y
. intro h1
simp[h1]
use x
exact ⟨ hx, fun a ↦ hx (h1 a) ⟩
theorem maximum_dc {a b : DCut} : ∀ (x y : ℚ), x ≤ y ∧ y ∈ a.A ∪ b.A → x ∈ a.A ∪ b.A := by
intro x y ⟨ hx, hy ⟩
apply Or.elim hy
. intro h
simp[h]
exact Or.inl (a.dc x y ⟨ hx, h ⟩)
. intro h
simp[h]
exact Or.inr (b.dc x y ⟨ hx, h ⟩)
theorem maximum_op {a b : DCut} : ∀ x ∈ a.A ∪ b.A, ∃ y ∈ a.A ∪ b.A, x < y:= by
intro x hx
apply Or.elim hx
. intro h
obtain ⟨ q, ⟨ hq1, hq2 ⟩ ⟩ := a.op x h
exact ⟨ q, ⟨ Set.mem_union_left b.A hq1, hq2 ⟩ ⟩
. intro h
obtain ⟨ q, ⟨ hq1, hq2 ⟩ ⟩ := b.op x h
exact ⟨ q, ⟨ Set.mem_union_right a.A hq1, hq2 ⟩ ⟩
def maximum (a b : DCut) : DCut :=
⟨ a.A ∪ b.A, maximum_ne, maximum_nf, maximum_dc, maximum_op ⟩
instance max_inst : Max DCut := ⟨ maximum ⟩
theorem max_gz (a: DCut) : 0 ≤ maximum 0 a := by
simp_all[le_inst,zero_rw,odown,maximum]
theorem max_sym {a b : DCut} : maximum a b = maximum b a := by
simp[maximum,Set.union_comm]
@[simp]
theorem max_pos {a : DCut} : 0 ≤ a → maximum 0 a = a := by
simp[maximum,le_inst,DCut.ext_iff]
@[simp]
theorem max_neg {a : DCut} : a ≤ 0 → maximum 0 a = 0 := by
simp[maximum,le_inst,DCut.ext_iff]
@[simp]
theorem max_pos_lt {a : DCut} : 0 < a → maximum 0 a = a := by
simp[maximum,lt_inst,DCut.ext_iff]
@[simp]
theorem max_neg_lt {a : DCut} : a < 0 → maximum 0 a = 0 := by
simp[maximum,lt_inst,DCut.ext_iff]
@[simp]
theorem max_self {a : DCut} : maximum a a = a := by
simp[maximum,lt_inst,DCut.ext_iff]
@[simp]
theorem max_pos_to_neg {a: DCut} (ha : 0 < a) : maximum 0 (-a) = 0 := by
simp[maximum,lt_inst,DCut.ext_iff,neg_inst,neg,preneg,zero_rw,odown]
intro x y hy z hz hxyz
have ⟨ q, ⟨ hq1, hq2 ⟩ ⟩ := non_neg_in_pos ha
have := a_lt_b hq1 hz
linarith
theorem neg_lt {a : DCut}: 0 < a ↔ -a < 0 := by
constructor
. simp_all[lt_inst]
intro h1 h2
apply And.intro
. intro h
exact h1 (Eq.symm h)
. intro x ⟨ q, ⟨ hq, ⟨ r, ⟨ hr, hqr ⟩ ⟩ ⟩ ⟩
have h4 := a_lt_b (h2 hq) hr
simp[zero_rw,odown]
linarith
. intro ⟨ h1, h2 ⟩
apply And.intro
. exact id (Ne.symm (neg_ne_zero.mp h1))
. by_contra h
have ⟨ z, ⟨ hz1, hz2 ⟩ ⟩ := Set.not_subset.mp h
simp[neg_inst,neg,preneg,zero_rw,odown] at h2
have := h2 0 z hz1 z hz2 (by linarith)
simp at this
theorem neg_le {a : DCut} : 0 ≤ a ↔ -a ≤ 0 := by
simp[le_of_lt,@neg_lt a,eq_comm]
theorem neg_lt' {a : DCut} : a < 0 ↔ 0 < -a := by
constructor
. intro ⟨ h1, h2 ⟩
apply And.intro
. simp[DCut.ext_iff] at h1 ⊢
exact h1
. intro q hq
simp[neg_inst,neg,preneg]
simp[zero_rw,odown] at hq
have : 0 ∉ a.A := by
intro h
have := h2 h
simp[zero_rw,odown] at this
exact ⟨ q, ⟨ hq, ⟨ 0, ⟨ this, by linarith ⟩ ⟩ ⟩ ⟩
. simp[lt_inst]
intro ha h
apply And.intro
. exact ha
. by_contra hnq
have ⟨ z, ⟨ hz1, hz2 ⟩ ⟩ := Set.not_subset.mp hnq
simp[neg_inst,neg,preneg,zero_rw,odown] at h hz2
have ⟨ s, ⟨ hs1, hs2 ⟩ ⟩ := a.op z hz1
have ⟨ q, ⟨ hq, ⟨ r, ⟨ hr1, hr2 ⟩ ⟩ ⟩ ⟩ := h (-s) (by linarith)
have : s < r := by exact a_lt_b hs1 hr1
linarith
theorem neg_le' {a : DCut} : a ≤ 0 ↔ 0 ≤ -a := by
simp[le_of_lt,@neg_lt' a,eq_comm]
theorem pos_neg_sum {a : DCut} : a = maximum 0 a - maximum 0 (-a) := by
by_cases h : 0 < a
. rw[max_pos h.right]
rw[max_neg (neg_le.mp h.right)]
exact Eq.symm (sub_zero a)
. have := trichotomy_lt 0 a
simp[not_gt_to_le] at h
apply Or.elim this
. intro h'
rw[max_pos_lt h']
have := neg_lt.mp h'
rw[max_neg_lt this]
simp
. intro h'
rw[max_neg h]
have := neg_le'.mp h
rw[max_pos this]
simp
@[simp]
theorem neg_max_zero_neg {a : DCut} (ha : a ≤ 0) : -maximum 0 (-a) = a := by
have : 0 ≤ -a := by
rw[neg_le'] at ha
exact ha
simp[max_pos,this]
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Multiplication
Multiplication of Dedekind cuts is straightfoward, but also fairly involved, or as Rudin says: "bothersome". The issue is dealing with both positive and negative cuts. Following Rudin, the development of the definitions proceeds as follows:
- Define multiplication on positive cuts.
- Extend multiplciation on positive cuts to non-negative cuts, building on the previous step by handling the cases in which either or both of the cuts is zero.
- Extend multiplication on non-negative cuts to all cuts.
For each of the above definitions of multiplication, we also prove the properties required to show that multiplication forms a commutiative group on cuts:
- 0 is an identity:
0*x = 0
- Multiplication is commutative:
x*y = y*x
- Associativity:
x*(y*z) = (x*y)*z
The last property is particularly challenging, because to relate arbitary reals with positive reals, one has to deal with an abundance of cases, preferably gracefully. Thus, along the way we will prove several intermediate results which allow us to make the proof more concise.
Definitions
Multiplication of Positive Cuts
Given two positive cuts 0 < a
and 0 < b
, their product is the set of points z
such that for some x ∈ a
and y ∈ b
, z < x*y
. This basic definition underlies the entire framework for multiplication, after which we simply have to deal with various combinations of non-negative and negative numbers.
def pre_mul (a b : DCut) :=
{ z | ∃ x ∈ a.A, ∃ y ∈ b.A, 0 < x ∧ 0 < y ∧ z < x*y }
To make some proofs more readable, it is useful to characterize pre_mul the following sufficient condition.
theorem pre_mul_suffice {a b : DCut} {x y z : ℚ}
: x ∈ a.A → y ∈ b.A → 0 < x → 0 < y → z < x*y → z ∈ pre_mul a b := by
intro hx hy _ _ _
use x, hx, y, hy
To prove that this definition results in a cut, we need to prove as usual that it is non-empty, not equal to ℚ, downward closed, and open.
First, we show pre_mul a b
is non-empty, by showing that it contains 0
. Since a
and b
are positive, they contain 0
. By the op
property, a
and b
must also contain values x
and y
larger than zero. Then 0 < x*y as well, satisfying the definition.
theorem pre_mul_ne {a b : DCut} (ha : 0 < a) (hb : 0 < b) : ∃ q, q ∈ pre_mul a b := by
have ⟨ x, ⟨ hx1, hx2 ⟩ ⟩ : ∃ x ∈ a.A, 0 < x := a.op 0 (zero_in_pos ha)
have ⟨ y, ⟨ hy1, hy2 ⟩ ⟩ : ∃ y ∈ b.A, 0 < y := b.op 0 (zero_in_pos hb)
use 0
apply pre_mul_suffice hx1 hy1 hx2 hy2
nlinarith
To show pre_mul a b
is not ℚ
, we choose x
and y
not in a
and b
respectively and show that x*y
is bigger than every value in in pre_mul a b
. Although this proof does not require that a
and b
are positive, we include these conditions anyway for consistency.
theorem pre_mul_nf {a b : DCut} (_ : 0 < a) (_ : 0 < b)
: ∃ q, q ∉ pre_mul a b := by
obtain ⟨ x, ⟨ hx, hx' ⟩ ⟩ := has_ub a
obtain ⟨ y, ⟨ hy, hy' ⟩ ⟩ := has_ub b
use x*y
apply ub_to_notin
intro q hq
choose s hs t ht hs0 ht0 hqst using hq
nlinarith[hx' s hs, hy' t ht]
That pre_mul a b
is downward closed is results from a straightforward unpacking of the definitions.
theorem pre_mul_dc {a b : DCut}
: ∀ x y, x ≤ y ∧ y ∈ (pre_mul a b) → x ∈ (pre_mul a b) := by
intro x y ⟨ hxy, hy ⟩
obtain ⟨ s, ⟨ hs, ⟨ t, ⟨ ht, ⟨ hsp, ⟨ htp, hyst ⟩ ⟩ ⟩ ⟩ ⟩ ⟩ := hy
exact pre_mul_suffice hs ht hsp htp (lt_of_le_of_lt hxy hyst)
To show pre_mul a b
is open, we start with values s
and t
in a
and b
respectively. Since a
and b
are open, we obtain values s'
and t'
in that are greater that s
and t
and still in a
and b
. Then s*t < s'*t'
as required.
theorem pre_mul_op {a b : DCut}
: ∀ x ∈ (pre_mul a b), ∃ y ∈ (pre_mul a b), x < y := by
intro x ⟨ s, ⟨ hs, ⟨ t, ⟨ ht, ⟨ hsp, ⟨ htp, hyst ⟩ ⟩ ⟩ ⟩ ⟩ ⟩
have ⟨ s', ⟨ hs', hss' ⟩ ⟩ := a.op s hs
have ⟨ t', ⟨ ht', htt' ⟩ ⟩ := b.op t ht
have h: s*t < s'*t' := by nlinarith
use! s*t, s', hs', t', ht'
split_ands
repeat
linarith
Grouping these properties together we have a proof that this definition of multiplication results in a proper cut.
def mul_pos (a b : DCut) (ha : 0 < a) (hb : 0 < b) : DCut :=
⟨ pre_mul a b, pre_mul_ne ha hb, pre_mul_nf ha hb, pre_mul_dc, pre_mul_op ⟩
We will need the following property to extend multiplication from positive numbers to non-negative numbers stating that the product of two positive numbers is again positive. Thus, the definition pre_mul
operates exclusively on the positive reals.
theorem zero_in_pre_mul {a b : DCut} (ha : 0 < a) (hb : 0 < b)
: 0 ∈ pre_mul a b := by
have ⟨ x, ⟨ hx1, hx2 ⟩ ⟩ := non_neg_in_pos ha
have ⟨ y, ⟨ hy1, hy2 ⟩ ⟩ := non_neg_in_pos hb
use! x, hx1, y, hy1, hx2, hy2
nlinarith
theorem mul_is_pos {a b : DCut} (ha : 0 < a) (hb : 0 < b)
: 0 < mul_pos a b ha hb := by
apply pos_has_zero.mpr
exact zero_in_pre_mul ha hb
Multiplication on Non-negative Cuts
We now extend the definition to non-negative reals, essentially dealing with the cases in which either cut is zero, in which case the resulting product is zero. Indeed, if one of a
and b
is zero, then pre_mul a b = ∅
.
example {A : Set ℕ}: A ⊆ ∅ ↔ A = ∅ := by exact Set.subset_empty_iff
@[simp]
theorem zero_mul_empty {a b : DCut} (h : 0 = a ∨ 0 = b) : pre_mul a b = ∅ := by
rcases h with h | h
repeat
. simp[pre_mul,←h,zero_rw,odown]
ext
simp
intros
linarith
Since ∅
is not a valid cut, we use pre_mul a b ∪ odown 0
to represent the product of two non-negative cuts. Of course, if a
and b
are positive cuts, then pre_mul a b
is downward closed, so the union with odown 0
is not needed.
@[simp]
theorem non_zero_mul_subsume {a b : DCut} (ha : 0 < a) (hb : 0 < b)
: pre_mul a b ∪ odown 0 = pre_mul a b := by
simp_all[lt_inst]
exact lt_imp_le (mul_is_pos ha hb)
The usual theorems are required to show that the product is a cut. We simply have to deal with the possible cases.
theorem mul_nneg_ne {a b : DCut}
: ∃ q, q ∈ pre_mul a b ∪ odown 0 := by
use -1
apply Or.inr
simp[odown]
theorem mul_nneg_nf {a b : DCut} (ha : 0 ≤ a) (hb : 0 ≤ b)
: ∃ q, q ∉ pre_mul a b ∪ odown 0 := by
rcases two_nn_ineqs ha hb with ⟨ ha, hb ⟩ | h | h
. simp[pre_mul_nf,ha,hb]
repeat
. simp[h,odown]
exact ⟨ 1, rfl ⟩
theorem mul_nneg_dc {a b : DCut} {x y : ℚ}
: x ≤ y ∧ y ∈ pre_mul a b ∪ odown 0 → x ∈ pre_mul a b ∪ odown 0 := by
intro ⟨ h1, h2 ⟩
apply Or.elim h2
. intro hy
apply Or.inl
exact pre_mul_dc x y ⟨ h1, hy ⟩
. intro hy
apply Or.inr
exact odown_dc x y ⟨ h1, hy ⟩
theorem mul_nneg_op {a b : DCut} (x : ℚ) :
x ∈ pre_mul a b ∪ odown 0 → ∃ y ∈ pre_mul a b ∪ odown 0, x < y := by
intro h
apply Or.elim h
. intro hx
have ⟨ q, ⟨ hq1, hq2 ⟩ ⟩ := pre_mul_op x hx
use q
exact ⟨ Or.inl hq1, hq2 ⟩
. intro hx
use x/2
simp_all[odown]
exact ⟨ Or.inr (by linarith), by linarith ⟩
def mul_nneg (a b : DCut) (ha : 0 ≤ a) (hb : 0 ≤ b) : DCut :=
⟨ pre_mul a b ∪ odown 0,
mul_nneg_ne,
mul_nneg_nf ha hb,
@mul_nneg_dc a b,
@mul_nneg_op a b ⟩
We note that when a
and b
are positive cuts, that mul_nneg
agrees with mul_pos
.
theorem nneg_eq_pos {a b : DCut} (ha : 0 < a) (hb : 0 < b)
: mul_nneg a b (lt_imp_le ha) (lt_imp_le hb) = mul_pos a b ha hb := by
simp_all[mul_is_pos,ha,hb,mul_nneg,mul_pos]
Mulitiplication on Arbitrary Cuts
Finally, we extend multiiplcation to arbitrary cuts. This step uses the fact that every cut a
can be written as the difference max 0 a - max 0 (-a)
. If 0 ≤ a
then
max 0 a - max 0 (-a) = a + 0
and if a < 0
then
max 0 a - max 0 (-a) = 0 + -a
both of which are non-negative, and we can therefore use the previous definition of multiplication on non-negative cuts.
def mul (a b : DCut) : DCut :=
let ap := maximum 0 a
let an := maximum 0 (-a)
let bp := maximum 0 b
let bn := maximum 0 (-b)
(mul_nneg ap bp (max_gz _) (max_gz _)) +
(mul_nneg an bn (max_gz _) (max_gz _)) -
(mul_nneg ap bn (max_gz _) (max_gz _)) -
(mul_nneg an bp (max_gz _) (max_gz _))
We may now instantiate the notation classes for multiplcation.
instance hmul_inst : HMul DCut DCut DCut := ⟨ mul ⟩
instance mul_inst : Mul DCut := ⟨ mul ⟩
example : (1:DCut) * 0 = 0 := by
simp[hmul_inst,mul]
Multiplication by 0
For non-negative cuts, it is useful to know that 0*a = 0
and a*0 = 0
, as these properties allow us to reason about the zero cases in the non-negative commutativity proof. These properties also allow us to show that 0
is the multiplicative identity, which is needed for proving cuts with multiplication form a group.
@[simp]
theorem mul_nneg_zero_left {a : DCut} (ha: 0 ≤ a)
: mul_nneg 0 a (λ _ a => a) ha = 0 := by
simp[mul_nneg,DCut.ext_iff,zero_rw]
@[simp]
theorem mul_nneg_zero_right {a : DCut} (ha: 0 ≤ a)
: mul_nneg a 0 ha (λ _ a => a) = 0 := by
simp[mul_nneg,DCut.ext_iff,zero_rw]
These two theorems allow us to prove that the multiple of two non-negative cuts is again non-negative.
@[simp]
theorem mul_is_nneg {a b : DCut} (ha : 0 ≤ a) (hb : 0 ≤ b)
: 0 ≤ mul_nneg a b ha hb := by
rcases two_nn_ineqs ha hb with ⟨ ha, hb ⟩ | h | h
. rw[nneg_eq_pos ha hb]
exact lt_imp_le (mul_is_pos ha hb)
repeat
. simp[←h]
simp_all[lt_of_le]
We can extend these properties to arbitrary cuts.
@[simp]
theorem mul_zero_left {a : DCut} : 0 * a = 0 := by
simp[hmul_inst,mul]
@[simp]
theorem mul_zero_right {a : DCut} : a * 0 = 0 := by
simp[hmul_inst,mul]
instance mul_zero_inst : MulZeroClass DCut := ⟨
@mul_zero_left,
@mul_zero_right
⟩
Commutativity
For positive cuts, commutativity is straightfoward, as it simply amounts to reordering x and y in the definition of pre_mul
.
theorem mul_pos_comm {a b : DCut} (ha : 0 < a) (hb : 0 < b)
: mul_pos a b ha hb = mul_pos b a hb ha := by
ext q
constructor
repeat
. intro ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, ⟨ h1, ⟨ h2, h3 ⟩ ⟩ ⟩ ⟩ ⟩ ⟩
exact ⟨ y, ⟨ hy, ⟨ x, ⟨ hx, ⟨ h2, ⟨ h1, by linarith ⟩ ⟩ ⟩ ⟩ ⟩ ⟩
Proving commutativity for non-negative cuts amounts to three cases, where a
and b
are both positive and where one or the other is negative.
theorem mul_nneg_comm {a b : DCut} (ha : 0 ≤ a) (hb : 0 ≤ b)
: mul_nneg a b ha hb = mul_nneg b a hb ha := by
rcases two_nn_ineqs ha hb with ⟨ ha, hb ⟩ | h | h
. simp[mul_nneg]
have := mul_pos_comm ha hb
simp_all[mul_pos]
repeat
. simp[h]
The proof of commutativity for arbitrary cuts requires us to reason about every possible combination of non-negative and negative cuts. We do this with the theorem two_ineqs_true
which enuerates all four cases. For each case, the same simplificiation works.
theorem mul_comm {a b : DCut}: a*b = b*a := by
rcases two_ineqs a b with ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩
repeat
simp[ha,hb,hmul_inst,mul,mul_nneg_comm,neg_le.mp]
Multiplication by 1
The proof that 1*x=x
is split into three main steps for positive, non-negative, and arbitary cuts. For positive cuts, the proof is split into two parts that show 1*a ≤ a
and a*1 ≤ 1
respectively. The first direction is straightforward, and uses the fact that a
is downward closed.
theorem mul_pos_id_left_1 {a : DCut} (ha: 0 < a)
: mul_pos 1 a zero_lt_one ha ≤ a := by
intro q ⟨ x, ⟨ hx, ⟨ y, ⟨ hy, ⟨ hx0, ⟨ hy0, hqxy ⟩ ⟩ ⟩ ⟩ ⟩ ⟩
apply a.dc q (x*y)
split_ands
. linarith
. have hxy : x*y < y := mul_lt_of_lt_one_left hy0 hx
exact a.dc (x*y) y ⟨ by linarith, hy ⟩
For the other direction, we assume we have q ∈ a.A
and need to show q
is in mul_pos 1 a
. This is done differently depending on whether q
is positive or non-negative. For the first case, we use the fact that a.A
is open to find s
and t
in a.A
with q < s < t
. Then q < s*t
as required. For q
non-negative, we use the fact that 0 ∈ a.A
and find s ∈ a.A
with 0<s
. We also have 1/2 ∈ odown 1
. Then q < s*(1/2)
as required.
theorem mul_pos_id_left_2 {a : DCut} (ha: 0 < a)
: a ≤ mul_pos 1 a zero_lt_one ha := by
intro q hq
simp[mul_pos]
by_cases h : 0 < q
. have ⟨ s, ⟨ t, ⟨ hx, ⟨ ht1, ⟨ hsq, st ⟩ ⟩ ⟩ ⟩ ⟩ := op2 q hq
have hs3 : 0 < s := by linarith
refine pre_mul_suffice ?_ ht1 (div_pos h hs3) ?_ ?_
. apply in_one
exact Bound.div_lt_one_of_pos_of_lt hs3 (by linarith)
. linarith
. have hts : t/s > 1 := (one_lt_div hs3).mpr (by linarith)
have hqts : q*(t/s) = q / s * t := Eq.symm (mul_comm_div q s t)
nlinarith
. have ⟨s, ⟨ hs1, hs2 ⟩ ⟩ := a.op 0 (zero_in_pos ha)
refine pre_mul_suffice ?_ hs1 one_half_pos hs2 ?_
. apply in_one
linarith
. linarith
Combining the above inequalities gives the main result for positive cuts.
@[simp]
theorem mul_pos_id_left {a : DCut} (ha: 0 < a)
: mul_pos 1 a zero_lt_one ha = a := by
apply PartialOrder.le_antisymm
. exact mul_pos_id_left_1 ha
. exact mul_pos_id_left_2 ha
For non-negative cuts, we consider the cases where 0 = a
and 0 < a
separately.
@[simp]
theorem mul_nneg_id_left {a : DCut} (ha: 0 ≤ a)
: mul_nneg 1 a zero_le_one ha = a := by
rw[le_of_lt] at ha
rcases ha with h1 | h2
. simp[←h1]
. have := mul_pos_id_left h2
simp_all[mul_pos,DCut.ext_iff,mul_nneg,DCut.ext_iff]
exact ha
Commutativity makes it easy to prove similar versions of theorems for which one side has already been established. For example:
@[simp]
theorem mul_nneg_id_right {a : DCut} (ha: 0 ≤ a)
: mul_nneg a 1 ha zero_le_one = a := by
rw[mul_nneg_comm,mul_nneg_id_left]
And similarly,
@[simp]
theorem mul_id_right {a : DCut} : a * 1 = a := by
simp only[hmul_inst,mul]
by_cases ha : 0 < a
. simp[ha]
. simp
rw[not_gt_to_le] at ha
simp[ha]
@[simp]
theorem mul_id_left {a : DCut} : 1 * a = a := by
simp[mul_comm]
Mathlib includes a class that keeps track of these properties.
instance mul_one_inst : MulOneClass DCut := ⟨
@mul_id_left,
@mul_id_right
⟩
Associativity
The proof that mul
is associatve amounts to a lot of book-keeping around some simple observations. We start with a proof that mul_pos
is associatve, which has two directions to prove. Each uses the fact that the cuts are open.
theorem mul_pos_assoc {a b c : DCut} (ha : 0 < a) (hb : 0 < b) (hc : 0 < c)
: mul_pos a (mul_pos b c hb hc) ha (mul_is_pos hb hc) =
mul_pos (mul_pos a b ha hb) c (mul_is_pos ha hb) hc := by
ext q
constructor
. choose x hx yz h' hx0 hyz0 hq
choose y hy z hz hy0 hz0 hyz' using h'
obtain ⟨ x', ⟨ hx', hxx' ⟩ ⟩ : ∃ x' ∈ a.A, x < x' := a.op x hx
use x*y
constructor
. use! x', hx', y
simp_all
nlinarith
. use z
simp_all
nlinarith
. choose xy h' z hz hxy hx0 hq
choose x hx y hy hz0 hy0 hxy' using h'
have ⟨ z', ⟨ hz', hzz' ⟩ ⟩ : ∃ z' ∈ c.A, z <z' := c.op z hz
use! x, hx, y*z
constructor
. use y, hy, z'
simp_all
nlinarith
. simp_all
nlinarith
Extending this result to non-negative cuts requires reasoning about four cases, convenienly available through the three_nn_ineqs
theorem.
@[simp]
theorem mul_nneg_assoc {a b c : DCut} (ha : 0 ≤ a) (hb : 0 ≤ b) (hc : 0 ≤ c)
: mul_nneg a (mul_nneg b c hb hc) ha (mul_is_nneg hb hc) =
mul_nneg (mul_nneg a b ha hb) c (mul_is_nneg ha hb) hc := by
rcases three_nn_ineqs ha hb hc with ⟨ ha', hb', hc' ⟩ | h | h | h
. simp[mul_nneg]
congr -- removes `∪ odown 0`
simpa[mul_pos,ha',hb',hc'] using mul_pos_assoc ha' hb' hc'
repeat
. simp[h]
To prove associativity in general, it is tempting to look at 27 possible cases in which each of three cuts are positive, zero or negative. However, we can take advantage of some basic algebra to reduce the number of cases to eight. To proceed, note that when a ≤ 0
while 0 ≤ b
and 0 ≤ c
, then
(a*b)*c = a*(b*c)
becomes
-((-a)*b)*c = -((-a)*(b*c))
and then use mul_assoc_all_nn. Similarly, we can do all the other cases this way.
@[simp]
theorem mul_neg_dist_left {a b : DCut} : a*(-b) = -(a*b) := by
simp[hmul_inst,mul]
rcases two_ineqs a b with ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩
repeat
simp[ha,hb,neg_le.mp]
@[simp]
theorem mul_neg_dist_right {a b : DCut} : (-a)*b = -(a*b) := by
simp only[hmul_inst,mul]
rcases two_ineqs a b with ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩ | ⟨ ha, hb ⟩
repeat
simp[ha,hb,neg_le.mp]
To make the proof more readable, we rewrite the theorem for non-negative cuts in terms of arbitrary cuts.
theorem mul_assoc_all_nn {a b c : DCut} (ha : 0 ≤ a) (hb : 0 ≤ b) (hc : 0 ≤ c)
: a * (b * c) = (a * b) * c := by
simp[hmul_inst,mul]
simp[ha,hb,hc,neg_le.mp] -- uses mul_nneg_assoc
And we prove a simple theorem that allows us to flip the direction of an inequality involving a negative cut.
theorem flip {a : DCut} (ha: a < 0) : 0 ≤ -a := neg_le'.mp (lt_imp_le ha)
Finally, combining the above, we can use the simplifier, flip
and mul_assoc_all_nn
to prove associativity for arbitrary cuts.
theorem mul_assoc {a b c : DCut} : a * (b * c) = (a * b) * c := by
rcases three_ineqs a b c with ⟨ ha, hb, hc ⟩ | ⟨ ha, hb, hc ⟩ |
⟨ ha, hb, hc ⟩ | ⟨ ha, hb, hc ⟩ |
⟨ ha, hb, hc ⟩ | ⟨ ha, hb, hc ⟩ |
⟨ ha, hb, hc ⟩ | ⟨ ha, hb, hc ⟩
. exact mul_assoc_all_nn ha hb hc
. simpa using mul_assoc_all_nn (flip ha) hb hc
. simpa using mul_assoc_all_nn ha (flip hb) hc
. simpa using mul_assoc_all_nn ha hb (flip hc)
. simpa using mul_assoc_all_nn (flip ha) (flip hb) hc
. simpa using mul_assoc_all_nn (flip ha) hb (flip hc)
. simpa using mul_assoc_all_nn ha (flip hb) (flip hc)
. simpa using mul_assoc_all_nn (flip ha) (flip hb) (flip hc)
Instantiating Multiplication Classes
With associatively and commutivity proved, we can show that multiplication forms a semigroup and a commutative magma.
instance semigroup_inst : Semigroup DCut := ⟨
λ x y z => Eq.symm (@mul_assoc x y z)
⟩
instance semigroup_w_zero_inst : SemigroupWithZero DCut := ⟨
@mul_zero_left,
@mul_zero_right
⟩
instance mul_zo_inst : MulZeroOneClass DCut := ⟨
@mul_zero_left,
@mul_zero_right
⟩
instance comm_magma_inst : CommMagma DCut := ⟨ @mul_comm ⟩
instance comm_semigroup_inst : CommSemigroup DCut := ⟨ @mul_comm ⟩
Natural Powers and Monoid Instance
Mathlib's class structure that leads to instantiating a type as a field includes showing the type is a Monoid, which includes a method for raising a cut x
to a natural numbered power, as in x^n
. We define that method here.
def npow (n: ℕ) (x : DCut) : DCut := match n with
| Nat.zero => 1
| Nat.succ k => x * (npow k x)
And show to obvious statements about such powers.
theorem npow_zero {x : DCut} : npow 0 x = 1 := by rfl
theorem npow_succ {n : ℕ} {x : DCut} : npow (n+1) x = npow n x * x := by
simp[npow,mul_comm]
Together these properties allow us to instante DCut as a Monoid, a Commutative Monoind, and a Commutative Monoid with zero.
instance monoid_inst : Monoid DCut := ⟨
@mul_id_left,
@mul_id_right, -- why does this need to be here if this is already a MulOneClass?
npow,
@npow_zero,
@npow_succ
⟩
instance com_monoid_inst : CommMonoid DCut := ⟨
@mul_comm
⟩
instance monoid_wz_inst : MonoidWithZero DCut := ⟨
@mul_zero_left,
@mul_zero_right
⟩
instance comm_monoid_wz_inst : CommMonoidWithZero DCut := ⟨
@mul_zero_left,
@mul_zero_right
⟩
Here is a simple example that uses theorems about monoids from Mathlib.
example (x : DCut) : x^2 = x*x := by
exact pow_two x
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Overview
An order relation on a set A
is a predicate A → A → Prop
that captures some notion of order. A familiar example is the the less than relation on the natural numbers:
#check 1 ≤ 2
where <
is shorthand for
#check Nat.le -- ℕ → ℕ → Prop
Nat.le
is an example of a total order on a set, meaning any two elements x
and y
are related (i.e. x≤y
or y≤x
). This need not be the case in general. For example, the subset relation ⊆
on sets is only a partial order, because one can find sets A
and B
for which neither A ⊆ B
or B ⊆ A
.
namespace Temp
def A : Set ℕ := {1,2}
def B : Set ℕ := {3,4}
example : ¬A ⊆ B ∧ ¬B ⊆ A := by
apply And.intro
. intro h
have h1a: 1 ∈ A := by simp[A]
have h1b := h h1a
simp[B] at h1b
. intro h
have h3b: 3 ∈ B := by simp[B]
have h3a := h h3b
simp[A] at h3a
end Temp
You will encounter many other examples of orderings besides these two, some of which we will get to in later sections. For now, we aim like to define a hierarchy of types of orders that capture their similarities and differences, defining a general theory of orders. A side goal here is to show how Lean's heirachy machinery works from the point of view of defining a new hierarchy instead of using someone else's hierarchy.
Most of this material comes from the book Introduction to Lattices and Order by Davey and Priestly.
Partial Orders
A partially ordered set or poset is a set and a less-than ordering relation on the set that requires pretty much the minimum one might expect from a binary relation for it to be called an ordering: the relation needs to be reflexive, anti-symmetric, and transitive (see Relations). Using a new Lean class
, we define a class of types that have a less-than relation with these three properties.
class Poset (α : Type u) where
le : α → α → Prop
refl : ∀ x, le x x
anti_sym : ∀ x y, le x y → le y x → x = y
trans : ∀ x y z, le x y → le y z → le x z
Example : The Natural Numbers
Lean's standard library has all of these properties defined for natural numbers. Therefore, we can assert that ℕ
is a poset
by instantiating the Poset
class as follows.
instance : Poset ℕ := ⟨ Nat.le, @Nat.le.refl, @Nat.le_antisymm, @Nat.le_trans⟩
Example : Sets
Lean's standard library also has all of these properties defined for sets.
instance {A: Type u} : Poset (Set A) := ⟨
Set.Subset,
Set.Subset.refl,
λ _ _ h1 h2 => Set.Subset.antisymm h1 h2,
λ _ _ _ h1 h2 => Set.Subset.trans h1 h2
⟩
Poset Notation
Simply having the Poset
class defined does not give us much, however. Thus, the main goal of this section is to develop theorems that, for example, apply to any Poset
, define specific kinds of Poset
, or that relate Posets
to each other.
To state these theorems cleaning, we first register some notation with Lean. Instantiating the LE
and LT
classes in Lean's standard library allow us to use ≤
, ≥
, <
, and ge
on elements of our Poset
type. Notice how these instances are declared. We have to supply a Type A
, and require that it has been instantiated as a Poset
.
instance le_inst {A : Type u} [Poset A] : LE A := ⟨ Poset.le ⟩
instance lt_inst {A : Type u} [Poset A] : LT A := ⟨ λ x y => x ≤ y ∧ x ≠ y ⟩
example {A : Type u} [Poset A] (x:A) := x ≥ x
Total Orders
A total order is a Poset
with the added requirement that any two elements are comparable.
def Comparable {P : Type u} [Poset P] (x y: P) := x ≤ y ∨ y ≤ x
class TotalOrder (T: Type u) extends Poset T where
comp: ∀ x y : T, Comparable x y
The natural numbers are a total order, which is shown via a theorem in Lean's standard library. :
instance nat_total_order : TotalOrder ℕ :=
⟨ Nat.le_total ⟩
Sets are not a total order, however.
example : ∃ x y : Set ℕ, ¬Comparable x y := by
apply Exists.intro {1}
apply Exists.intro {2}
simp[Comparable]
(Meet) Semilattices
A Semilattice
is a Poset
for which there exists a greatest lower bound function, usually called meet
, for every pair of points x
and y
. Then we extend the hierarchy with a new class of orders.
class Semilattice (L : Type u) extends Poset L where
meet : L → L → L
lb : ∀ x y, meet x y ≤ x ∧ meet x y ≤ y
greatest : ∀ x y w, w ≤ x → w ≤ y → w ≤ meet x y
For example, the natural numbers form a semilattice. So do sets.
instance nat_semi_lattice : Semilattice ℕ :=
⟨
Nat.min,
by
intro x y
exact ⟨ Nat.min_le_left x y, Nat.min_le_right x y⟩,
by
intro x y _ h1 h2
exact Nat.le_min_of_le_of_le h1 h2
⟩
instance set_semi_lattice {α : Type u}: Semilattice (Set α) :=
⟨
Set.inter,
by
intro A B
apply And.intro
. intro x hx
exact Set.mem_of_mem_inter_left hx
. intro x hx
exact Set.mem_of_mem_inter_right hx,
by
intro A B _ h1 h2 _ hc
exact ⟨ h1 hc, h2 hc ⟩
⟩
Lattices
If all pairs of elements also have a least upper bound, then the Poset
is called a Lattice
. The least upper bound function is called the join.
class Lattice (L : Type u) extends Semilattice L where
join : L → L → L
ub : ∀ x y, (x ≤ join x y ∧ y ≤ join x y)
least : ∀ x y w, x ≤ w → y ≤ w → join x y ≤ w
Both ℕ and Sets are Lattices as well. The joing for ℕ is Nat.max
and the join for sets is Set.union
.
instance nat_lattice : Lattice ℕ :=
⟨
Nat.max,
by
intro x y
exact ⟨ Nat.le_max_left x y, Nat.le_max_right x y ⟩,
by
intro x y _ h1 h2
exact Nat.max_le_of_le_of_le h1 h2
⟩
instance set_lattice {α : Type u}: Lattice (Set α) :=
⟨
Set.union,
by
intro A B
. exact Set.union_subset_iff.mp (λ _ a => a),
by
intro A B C h1 h2 c hc
apply Or.elim hc
. exact λ h3 => h1 h3
. exact λ h3 => h2 h3
⟩
As an example of a semilattice that is not a lattice is the so-called information ordering on partial functions, decribed in a separate chapter.
Notation for Lattices
The meet and join of two elements x
and y
of a poset are denonted x ⊓ y
and x sup y
. The notation classes for these operations are called Min
and Max
, even though you do not have to use them for actual mins and maxes.
instance Semilattice.and_inst {L : Type u} [Semilattice L] : Min L :=
⟨ meet ⟩
instance Lattice.or_inst {L : Type u} [Lattice L] : Max L :=
⟨ join ⟩
Meet and Join Example Theorems
Here are two straightforward theorems about meets and joins that test out the definitions and notation. They follow from the definitions of greatest lower bound, least upper bound, anti-symmetry, and reflexivity.
theorem Semilattice.meet_idempotent {L : Type u} [Semilattice L] (x : L) : x ⊓ x = x := by
have ⟨ h1, h2 ⟩ := lb x x
have h4 := greatest x x x (Poset.refl x) (Poset.refl x)
exact Poset.anti_sym (x ⊓ x) x h1 h4
theorem Lattice.join_idempotent {L : Type u} [Lattice L] (x : L) : x ⊔ x = x := by
have ⟨ h1, h2 ⟩ := ub x x
have h4 := least x x x (Poset.refl x) (Poset.refl x)
apply Poset.anti_sym (x ⊔ x) x h4 h1
Complete Lattices
Lattices require that every pair of elements have a greatest lower bound and leaset upper bound. A Complete Lattice requires that every set have such bounds. An example of a Complete Lattice is Set A
, which we show after defining Complete Lattices.
def IsLB {P : Type u} [Poset P] (S : Set P) (lb : P) := ∀ x ∈ S, lb ≤ x
class CompleteSemilattice (L : Type u) extends Poset L where
inf : Set L → L
lb : ∀ S, IsLB S (inf S)
greatest : ∀ S w, (IsLB S w) → w ≤ inf S
def IsUB {P : Type u} [Poset P] (S : Set P) (ub : P) := ∀ x, x ∈ S → x ≤ ub
class CompleteLattice (L : Type u) extends CompleteSemilattice L where
sup : Set L → L
ub : ∀ S, IsUB S (sup S)
least : ∀ S, ∀ w, (IsUB S w) → sup S ≤ w
Example: The set of subsets of a given set A
is a complete lattice, which we show in two steps using straighforward proofs.
instance set_csl {A : Type u}: CompleteSemilattice (Set A) :=
⟨
λ S => { x | ∀ T ∈ S, x ∈ T },
by
intro S T h x hx
dsimp at hx
exact hx T h,
by
intro S T h x hx R hR
exact h R hR hx
⟩
instance set_cl {A : Type u}: CompleteLattice (Set A) :=
⟨
λ S => { x | ∃ T ∈ S, x ∈ T },
by
intro S T h x hx
apply Exists.intro T
exact ⟨h, hx⟩,
by
intro S T h x hx
dsimp at hx
obtain ⟨ R, ⟨ h1, h2 ⟩ ⟩ := hx
exact h R h1 h2
⟩
Complete Lattices are Bounded
Notice that in the definition of inf
the condition (IsLB S w)
in (IsLB S w)→ w ≤ inf S
is trivially satisfied if S = ∅
. Therefore, w ≤ inf ∅
for all w
, meaning that inf ∅
is a top element. Similarly, sup ∅
is a bottom element. We can conclude that every Complete Lattice is bounded, as shown by the next two theorems.
@[simp]
def CompleteLattice.bot {L : Type u} [CompleteLattice L] : L :=
sup (∅:Set L)
@[simp]
def CompleteLattice.top {L : Type u} [CompleteLattice L] : L :=
CompleteSemilattice.inf (∅:Set L)
theorem CompleteLattice.is_bot {L : Type u} [CompleteLattice L]
: ∀ x : L, bot ≤ x := by
intro x
apply CompleteLattice.least ∅ x
simp[IsUB]
theorem CompleteLattice.is_top {L : Type u} [CompleteLattice L]
: ∀ x : L, x ≤ top := by
intro x
apply CompleteSemilattice.greatest ∅ x
simp[IsLB]
Complete Lattices are Lattices
We can also show that a complete lattice is a lattice by restricting inf
and sup
to act on sets of size two.
instance CompleteSemilattice.inst_sl {L : Type u} [CompleteSemilattice L]
: Semilattice L := ⟨
λ x y => inf {x,y},
by
intro x y
apply And.intro <;>
apply lb <;>
simp,
by
intro x u z h1 h2
apply greatest
simp[IsLB, h1, h2]
⟩
instance CompleteLattice.inst_l {L : Type u} [CompleteLattice L]
: Lattice L := ⟨
λ x y => sup {x,y},
by
intro x y
apply And.intro <;>
apply ub <;>
simp,
by
intro x u z h1 h2
apply least
simp[IsUB, h1, h2]
⟩
Hierarchy
Lattice CL
| |
Semilattice CSL Total Order
\ | /
Poset
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Simple Properties
theorem eq_to_le {P : Type u} [Poset P] {x y : P} : x = y → x ≤ y := by
intro h
rw[h]
exact refl y
Up Sets and Down Sets
The set of all elements above (below) a given element x:P
is called the up (down) set of x
.
def up {P : Type u} [Poset P] (x : P) : Set P := { y | x ≤ y }
def down {P : Type u} [Poset P] (x : P) : Set P := { y | y ≤ x }
A set that is upwardly (downwardly) closed is called an Up (Down) set. We define predicates on subsets of a Poset to capture these properties. These are a bit tricky to read. The first one says that if x
is any element and there is a y
in some upward closed set S
that is less than or equal to it, then x
must also be in S
. The second statement about downward closed sets is similar.
def UpSet {P : Type u} [Poset P] (S : Set P) := ∀ x, (∃ y ∈ S, y ≤ x) → x ∈ S
def DownSet {P : Type u} [Poset P] (S : Set P) := ∀ x, (∃ y ∈ S, x ≤ y) → x ∈ S
Simple theorems relating these definitions can now be proved. The next two, for example, show that up (down) sets are upwardly (downwardly) closed.
theorem up_is_up {P : Type u} [Poset P] (x : P) : UpSet (up x) := by
intro z ⟨ y, ⟨ h1, h2 ⟩ ⟩
simp_all[Set.mem_def,up]
exact trans x y z h1 h2
theorem down_is_down {P : Type u} [Poset P] (x : P) : DownSet (down x) := by
intro z ⟨ y, ⟨ h1, h2 ⟩ ⟩
simp_all[Set.mem_def,down]
apply trans z y x h2 h1
Upward closed sets are not just those built from a single element. For example, the union of two upwardly closed sets is also upwardly closed.
theorem up_union {P : Type u} [Poset P] (x y: P) : UpSet ((up x) ∪ (up y)) := by
intro w ⟨ z, ⟨ h1, h2 ⟩ ⟩
apply Or.elim h1
. intro h3
exact Or.inl (trans x z w h3 h2)
. intro h3
apply Or.inr (trans y z w h3 h2)
Lower and Upper Sets
def upper {P : Type u} [Poset P] (A : Set P) : Set P :=
{ x | ∀ a ∈ A, a ≤ x }
def lower {P : Type u} [Poset P] (A : Set P) : Set P :=
{ x | ∀ a ∈ A, x ≤ a }
-- 1
theorem sub_ul {P : Type u} [Poset P] (A : Set P)
: A ⊆ upper (lower A) := by
intro x hx a ha
exact ha x hx
theorem sub_lu {P : Type u} [Poset P] (A : Set P)
: A ⊆ lower (upper A) := by
intro x hx a ha
exact ha x hx
theorem eq_to_sub {P : Type u} [Poset P] (A : Set P)
: lower (upper A) ⊆ A → lower (upper A) = A := by
intro h
exact Set.eq_of_subset_of_subset h (sub_lu A)
-- 2
theorem sub_up {P : Type u} [Poset P] {A B : Set P}
: A ⊆ B → upper B ⊆ upper A := by
intro h b hb a ha
exact hb a (h ha)
-- 3
theorem sub_low {P : Type u} [Poset P] {A B : Set P}
: A ⊆ B → lower B ⊆ lower A := by
intro h b hb a ha
exact hb a (h ha)
-- 4
theorem up_ulu {P : Type u} [Poset P] {A : Set P}
: upper A = upper (lower (upper A)) := by
apply Set.eq_of_subset_of_subset
. intro a ha b hb
exact hb a ha
. intro a ha b hb
exact ha b fun a a ↦ a b hb
-- 5
theorem low_lul {P : Type u} [Poset P] {A : Set P}
: lower A = lower (upper (lower A)) := by
apply Set.eq_of_subset_of_subset
. intro a ha b hb
exact hb a ha
. intro a ha b hb
exact ha b fun a a ↦ a b hb
Minimal and Maximal Elements
A minimal element of a set S ⊆ P
is one for which no other elements of S
are smaller.
def Minimal {P : Type u} [Poset P] (S : Set P) (m : P) := ∀ x ∈ S, x ≤ m → x = m
Minimal elements are not necessarily unique. The following example shows that when x
and y
are unrelated, either one of them is minimal.
example {P : Type u} [Poset P] (x y: P) : (¬x≤y ∧ ¬y≤x) → Minimal {x,y} x := by
intro ⟨h1, h2⟩ z h3 h4
apply Or.elim h3
. exact id
. intro h5
rw[h5] at h4
exact False.elim (h2 h4)
On the other hand, a minimum element is a unique minimal element.
def Minimum {P : Type u} [Poset P] (S : Set P) (m : P) := ∀ x ∈ S, m ≤ x
The most minimal element of a Poset
is usually called bot
.
def is_bot {P : Type u} [Poset P] (x : P) := ∀ y, x ≤ y
theorem bot_minimum {P : Type u} [Poset P] (m : P) : is_bot m → Minimum Set.univ m := by
intro hb x hm
simp_all[is_bot]
These definitions apply to maxima as well.
def Maximal {P : Type u} [Poset P] (S : Set P) (m : P) := ∀ x ∈ S , m ≤ x → x = m
def Maximum {P : Type u} [Poset P] (S : Set P) (m : P) := ∀ x ∈ S, x ≤ m
def is_top {P : Type u} [Poset P] (x : P) := ∀ y, y ≤ x
Chains and Anti-Chains
A chain is a totally ordered subset of a poset.
def Chain {P : Type u} [Poset P] (S : Set P) := ∀ x ∈ S, ∀ y ∈ S, x ≤ y ∨ y ≤ x
For example, the upset of any natural number is a chain.
example {n : ℕ} : Chain (up n) := by
intro x hx y hy
exact Nat.le_total x y
An antichain is a set of uncomparable elements.
def AntiChain {P : Type u} [Poset P] (S : Set P) := ∀ x ∈ S, ∀ y ∈ S, x ≠ y → (¬x ≤ y ∧ ¬y ≤ x)
For example, the set of singletons each containing a different natural number is an anti-chain.
def my_anti_chain : Set (Set ℕ) := { {n} | n : ℕ }
example : AntiChain my_anti_chain := by
simp[my_anti_chain]
intro Sm ⟨ m, hsm ⟩ Sn ⟨ n, hsn ⟩ hmn
simp[le_inst]
apply And.intro
. intro h
rw[←hsm,←hsn] at h
rw[←hsm,←hsn] at hmn
exact hmn (congrArg singleton (h rfl))
. intro h
rw[←hsm,←hsn] at h
rw[←hsm,←hsn] at hmn
exact id (Ne.symm hmn) (congrArg singleton (h rfl))
Exercises
- Show the rational numbers ℚ with the natural order ≤ form a Poset
instance : Poset ℚ := ⟨ λ x y => x ≤ y, sorry, sorry, sorry ⟩
- Show the upper set (0,1) is [1,∞)
example : upper {x:ℚ | 0 < x ∧ x < 1} = { x | 1 ≤ x } := sorry
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Maps Between Posets
def OrderPreserving {P Q : Type u} [Poset P] [Poset Q] (φ : P → Q) :=
∀ x y : P, x ≤ y → φ x ≤ φ y
def OrderEmbedding {P Q : Type u} [Poset P] [Poset Q] (φ : P → Q) :=
∀ x y : P, x ≤ y ↔ φ x ≤ φ y
def OneToOne {P Q : Type u} (φ : P → Q) :=
∀ x y , φ x = φ y → x = y
def Onto {P Q : Type u} (φ : P → Q) :=
∀ y , ∃ x , φ x = y
def OrderIsomorphism {P Q : Type u} [Poset P] [Poset Q] (φ : P → Q) :=
Onto φ ∧ OrderEmbedding φ
theorem order_pres_comp {P Q R : Type u} [Poset P] [Poset Q] [Poset R] (φ : P → Q) (ψ : Q → R)
: OrderPreserving φ → OrderPreserving ψ → OrderPreserving (ψ ∘ φ) := by
intro h1 h2 x y hxy
apply h2 (φ x) (φ y)
apply h1 x y
exact hxy
theorem order_embed_1to1 {P Q : Type u} [Poset P] [Poset Q] (φ : P → Q)
: OrderEmbedding φ → OneToOne φ := by
intro h x y hxy
apply anti_sym
. apply (h x y).mpr
exact eq_to_le hxy
. apply (h y x).mpr
exact eq_to_le (Eq.symm hxy)
theorem order_iso_bijective {P Q : Type u} [Poset P] [Poset Q] (φ : P → Q)
: OrderIsomorphism φ → (OneToOne φ ∧ Onto φ) := by
intro ⟨ h1, h2 ⟩
exact ⟨ order_embed_1to1 φ h2, h1 ⟩
Examples
example : OrderPreserving (λ _ : ℕ => 0) := by
intro x y h
dsimp
rfl
def f (n:ℕ) : Set ℕ := { x | x ≤ n }
example : OrderEmbedding f := by
intro x y
constructor
. intro h
intro z hz
exact trans z x y hz h
. intro h
simp[f] at h
exact h x (Poset.refl x)
def g (x : ℕ) : ℕ := 2*x
example : OrderEmbedding g := by
intro x y
constructor
. intro h
simp[g]
exact h
. intro h
simp[g] at h
exact h
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Strings
open List
Note that List already have an ordering on them, but it is not the prefix ordering. This creates issues because List.le and [LT (List A)] are already defined. We avoid this here by just not using the ≤ notation.
#check List.le
variable {A : Type u} (x : List A) [LT A]
#check [] ≤ x
#eval [1,2,3] ≤ [1,2,5]
example {A : Type u} [LT A] (x : List A) : [] ≤ x := by
simp[List.instLE]
End Note
instance list_poset {A : Type u} : Poset (List A) :=
⟨
IsPrefix,
List.prefix_refl,
by
intro s t ⟨ x, h1 ⟩ ⟨ y, h2 ⟩
aesop,
by
intro s t u
exact List.IsPrefix.trans
⟩
example {A : Type u} (x : List A) : [] <+: x := by
simp[list_poset]
def meet {A : Type u} [Poset (List A)] [DecidableEq A] (x y : List A) : List A :=
match x,y with
| [], [] => []
| _, [] => []
| [], _ => []
| a::L, b::M => if a = b then a :: meet L M else []
theorem bot_le {A : Type u} [Poset (List A)] (x: List A) : [] <+: x := by
exact nil_prefix
theorem meet_le {A : Type u} [Poset (List A)] [DecidableEq A] (x y : List A)
: (meet x y) <+: x := by
match x,y with
| a::L, b::M =>
simp[meet]
split_ifs
. have : IsPrefix (meet L M) L := by apply meet_le -- structural recursion!
simp[this]
. apply bot_le
| [], [] => simp[meet]
| a::L, [] => simp[meet]
| [], b::M => simp[meet]
theorem meet_le' {A : Type u} [Poset (List A)] [DecidableEq A] (x y : List A)
: (meet x y) <+: y := by
match x,y with
| a::L, b::M =>
simp[meet]
split_ifs
. have : IsPrefix (meet L M) M := by apply meet_le' -- structural recursion!
simp[this]
assumption
. apply bot_le
| [], [] => simp[meet]
| a::L, [] => simp[meet]
| [], b::M => simp[meet]
theorem meet_great {A : Type u} [Poset (List A)] [DecidableEq A] (x y : List A)
: ∀ w, w <+: x → w <+: y → w <+: (meet x y) := by
intro w hwx hwy
match x, y with
| a::L, b::M =>
simp[meet]
split_ifs
. rename_i hab
simp_all[hab]
by_cases hw : w = []
. simp[hw]
. obtain ⟨ c, ⟨ N, hcN ⟩ ⟩ := ne_nil_iff_exists_cons.mp hw
simp_all[hcN]
apply meet_great -- structural recursion!
. exact hwx.right
. exact hwy
. rename_i hab
have : w = [] := by
by_cases h : w = []
. exact h
. have ⟨ c, ⟨ N, hN ⟩ ⟩ : ∃ c, ∃ N, w = c :: N := ne_nil_iff_exists_cons.mp h
have h1 : c = a := by simp_all[hN]
have h2 : c = b := by simp_all[hN]
simp_all
simp[this]
| [], [] => simp[meet,hwx]
| a::L, [] => simp[meet,hwy]
| [], b::M => simp[meet,hwx]
instance info_semilattice {A : Type u} [DecidableEq A] : Semilattice (List A) :=
⟨
meet,
λ L M => by
constructor
. constructor
. apply meet_le
. apply meet_le'
. apply meet_great
⟩
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Information Ordering on Values
def PartialFunction (A : Type u) (B : Type v) := A → Option B
namespace PartialFunction
def val_le (B : Type u) (u v : Option B) := u = none ∨ u = v
theorem val_le_refl {B : Type u} : Refl (val_le B) := by
simp[Refl,val_le]
theorem val_le_anti_sym {B : Type u} : AntiSym (val_le B) := by
intro x y h1 h2
simp_all[val_le]
aesop
theorem val_le_trans {B : Type u} : Trans (val_le B) := by
intro x y z h1 h2
simp_all[val_le]
aesop
@[simp]
instance poset_val_le {B : Type u} : Poset (Option B) :=
⟨ val_le B, val_le_refl, val_le_anti_sym, val_le_trans ⟩
theorem none_val_le {B : Type u} : ∀ x : Option B, none ≤ x := by
intro x
have : val_le B none x := by simp[val_le]
simp[Poset.le_inst,val_le]
def Dom {A : Type u} {B : Type v} (f: PartialFunction A B) : Set A := λ a => f a ≠ none
def le {A : Type u} {B : Type v} : Relation (PartialFunction A B) (PartialFunction A B) :=
λ f g => ∀ x, (f x) ≤ (g x)
def bot {A : Type u} {B : Type v} : PartialFunction A B := λ _ => none
theorem le_refl {A : Type u} {B : Type v} : Refl (@le A B) := by
intro x h
apply val_le_refl
theorem le_anti_sym {A : Type u} {B : Type v} : AntiSym (@le A B) := by
simp[AntiSym]
intro f g hf hg
funext x
apply val_le_anti_sym
. exact hf x
. exact hg x
theorem le_trans {A : Type u} {B : Type v} : Trans (@le A B) := by
simp[Trans]
intro f g h hf hg
simp[le]
intro x
apply val_le_trans
. exact hf x
. exact hg x
instance poset_le (A : Type u) (B : Type v): Poset (PartialFunction A B) :=
⟨ le, le_refl, le_anti_sym, le_trans ⟩
theorem le_def {A : Type u} {B : Type v} (f g: PartialFunction A B)
: f ≤ g ↔ ∀ x, f x ≤ g x := by
simp[le_inst, Poset.le, le, val_le]
theorem le_total_def {A : Type u} {B : Type v} (f g: PartialFunction A B)
: f ≤ g ↔ ∀ x, f x = none ∨ f x = g x := by
simp[le_inst, Poset.le, le, val_le]
theorem bot_le_all {A : Type u} {B : Type v} (f: PartialFunction A B) : bot ≤ f := by
intro x
simp[val_le,bot,Poset.le_inst]
theorem le_dom {A : Type u} {B : Type v} {f g : PartialFunction A B} :
f ≤ g ↔ (Dom f ⊆ Dom g ∧ ∀ x ∈ Dom f, f x = g x):= by
constructor
. intro h
constructor
. intro a ha
have := h a
simp_all[Set.mem_def,Dom,val_le,Poset.le_inst]
. intro a ha
have := h a
simp_all[Set.mem_def,Dom,Poset.le,val_le,le,Poset.le_inst]
. intro ⟨ h1, h2 ⟩
intro a
have := h2 a
simp_all[Set.mem_def,Dom,Poset.le,val_le,le]
by_cases h3 : f a = none
. exact Or.inl h3
. apply Or.inr
exact this h3
example (A : Type u) (B : Type v) (f : PartialFunction A B) : f ≤ f := by apply Poset.refl
Total Information Elements
def Total {A : Type u} {B : Type v} (f : PartialFunction A B) := ∀ x, f x ≠ none
theorem total_le {A : Type u} {B : Type v} {f g : PartialFunction A B}
: Total f → Poset.le f g → f = g := by
simp[Total,Poset.le]
intro h hfg
funext x
have h1 := h x
have h2 := hfg x
simp_all[val_le,Poset.le,Poset.le_inst]
Information Ordering is a Lattice
def meet {A : Type u} {B : Type v} [DecidableEq B] (f g : PartialFunction A B) :=
λ a => if f a = g a then f a else none
theorem meet_symm {A : Type u} {B : Type v} [DecidableEq B] {f g : PartialFunction A B}
: meet f g = meet g f := by
unfold meet
aesop
theorem pf_meet_le {A : Type u} {B : Type v} [DecidableEq B] {f g : PartialFunction A B} :
(meet f g) ≤ f := by
intro a
simp[meet]
split_ifs <;>
simp[val_le,Poset.le_inst]
theorem pf_meet_le' {A : Type u} {B : Type v} [DecidableEq B] {f g : PartialFunction A B} :
le (meet f g) g := by
rw[meet_symm]
exact pf_meet_le
theorem pf_meet_greater {A : Type u} {B : Type v} [DecidableEq B] {f g : PartialFunction A B} :
∀ w, le w f → le w g → le w (meet f g) := by
intro w h1 h2 a
simp[meet,val_le]
split_ifs
. exact h1 a
. simp[Poset.le_inst,Poset.le,val_le,Poset.le_inst]
apply Or.elim (h2 a)
. exact id
. intro h3
cases h1 a with
| inl h => simp_all
| inr h => simp_all
instance info_semilattice {A : Type u} {B : Type v} [DecidableEq B] : Semilattice (PartialFunction A B) :=
⟨
meet,
λ _ _ => ⟨ ⟨ pf_meet_le, pf_meet_le' ⟩, pf_meet_greater ⟩
⟩
end PartialFunction
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
The Dedekind-MacNeille Completion
A completion is an embedding of a partially ordered set into a complete lattice. It allows one to "fill in the gaps" in an ordered set so that, for example, limit points exist. The real numbers, for example, are the completion of the rational numbers.
In this chapter we describe the the Dedekind-MacNeille (DM) Completion, which is a generalization of the Dedekind cuts method of constructing the reals to the case of any ordered set. In particular, we define DM P
for any poset P
. If P=ℚ
, the result is isomorphic to the reals with -∞
and ∞
, but the approach works for any poset.
Formally, the Dedekind-MacNeille completion DM P
is defined to be the family of subsets of S ⊆ P
that are closed with respect to the closure operator λ P ↦ lower (upper P)
.
@[ext]
structure DM (P : Type u) [Poset P] where
val : Set P
h : lower (upper val) = val
Our goal is to show that DM P
is a complete lattice for any P
. We can easily show that DM P
is a poset under the usual ⊆
ordering.
instance DM.poset {P : Type u} [Poset P] : Poset (DM P) :=
⟨
λ ⟨ A, hA ⟩ ⟨ B, hB ⟩ => A ⊆ B,
by
intro ⟨ A, _ ⟩
exact Set.Subset.refl A,
by
intro ⟨ A, hA ⟩ ⟨ B, hB ⟩ h1 h2
simp_all
exact Set.Subset.antisymm h1 h2,
by
intro ⟨ A, hA ⟩ ⟨ B, hB ⟩ ⟨ C, hC ⟩ h1 h2
exact Set.Subset.trans h1 h2
⟩
In fact, the DM
structure forms what Davey and Priestly call a topped intersection structure, which we will show is a Complete Lattice with a particular definition for the meet and join that we define next.
The Meet
We define a meet for DM P
, which is just set-intersection taken over a subset of DM P
.
$$ \mathrm{meet}(S) = \bigcap_{A ∈ S} A. $$
To prove this definition of meet gives DM P
a semilattice structure, we have to show the result of such an intersection satisfies the upper-lower
condition. First we define the intersection of a subset of DM P
(i.e. of a set of sets taken from DM P
).
def DM.inter {P : Type u} [Poset P] (S : Set (DM P)) := { x | ∀ T ∈ S, x ∈ T.val }
We will need to use the simple fact that the interection of a family ot sets is a subset of every member of the family.
theorem DM.inter_sub {P : Type u} [Poset P] {S : Set (DM P)}
: ∀ T ∈ S, inter S ⊆ T.val := by
intro T hT x hx
exact hx T hT
Using this fact, we can show the intersection preserves the lower-upper
property required of elements of DM P
.
theorem DM.inter_in_dm {P : Type u} [Poset P] {S : Set (DM P)}
: lower (upper (inter S)) = inter S := by
apply Set.eq_of_subset_of_subset
. intro x hx T hT
rw[←T.h]
exact sub_low (sub_up (inter_sub T hT)) hx
. exact sub_lu (inter S)
And with this theorem we can finally define the meet as a function from Set (DM P)
into DM P
. Recall, that to do so we need to not only supply the operation inter
on S
, but also proof that inter S
is in DM P
.
def DM.meet {P : Type u} [Poset P] (S : Set (DM P)) : DM P :=
⟨ inter S, inter_in_dm ⟩
To show that DM P
is a Complete Semilattice, we need to show that this definition of meet
is indeed a greatest lower bound. We do so in two steps, first showing the meet S
is a lower bound of S
and second showing it is a greatest lower bound of S
.
theorem DM.meet_lb {P : Type u} [Poset P] :
∀ S : Set (DM P), IsLB S (meet S) := by
intro S T hT
apply DM.inter_sub
exact hT
theorem DM.meet_greatest {P : Type u} [Poset P]
: ∀ S : Set (DM P), ∀ w, (IsLB S w) → w ≤ meet S := by
intro S W h
intro x hx T hT
exact h T hT hx
Now we have everything we need to instantiate the Complete Semilattice class.
instance DM.csl {P : Type u} [Poset P] : CompleteSemilattice (DM P) :=
⟨ meet, meet_lb, meet_greatest ⟩
The Join
Next we define a join. It would be nice to simply define the join of S
to be the union of all sets in S
, but the result would in general not be closed with respect to the lower-upper
operator used to define DM P
. To get around this, the join for DM P
is defined to be the intersection of sets containing the union.
$$ \mathrm{join}(S) = \bigcap \left \{ B \in DM(P) \;|\; \bigcup_{A ∈ S} A \subseteq B \right \} $$
First we define the union.
def DM.union {P : Type u} [Poset P] (S : Set (DM P)) := { x | ∃ T ∈ S, x ∈ T.val }
We will need an intermediate theorem analogous to the intersection theorem proved for the meet. This one shows that the intersection of a set of sets is contained in each set.
theorem DM.inter_union_dm {P : Type u} [Poset P] {S : Set (DM P)}
: ∀ C ∈ {C : DM P| union S ⊆ C.val}, inter {C | union S ⊆ C.val} ⊆ C.val := by
intro C hC x hx
exact hx C hC
We use this theorem to show the meet is closed.
theorem DM.union_in_dm {P : Type u} [Poset P] {S : Set (DM P)}
: lower (upper (inter {C | union S ⊆ C.val})) = inter {C | union S ⊆ C.val} := by
apply Set.eq_of_subset_of_subset
. intro x hx T hT
rw[←T.h]
exact sub_low (sub_up (inter_union_dm T hT)) hx
. apply sub_lu
The join operator is then be defined as follows.
def DM.join {P : Type u} [Poset P] (S : Set (DM P)) : DM P :=
⟨ inter { C | union S ⊆ C.val }, union_in_dm ⟩
To show DM P
is a Complete Lattice, we need to show the join is a least upper bound, which we do in two steps.
theorem DM.join_ub {P : Type u} [Poset P] :
∀ S : Set (DM P), IsUB S (join S) := by
intro S T hT x hx W hW
simp[union,Set.subset_def] at hW
exact hW x T hT hx
theorem DM.join_least {P : Type u} [Poset P]
: ∀ S : Set (DM P), ∀ W, (IsUB S W) → join S ≤ W := by
intro S W h x hx
apply hx W
intro y ⟨ Q, ⟨ h1, h2 ⟩ ⟩
exact h Q h1 h2
Now we have everything we need to show DM P
is a Complete Lattice.
instance DM.lattice {P : Type u} [Poset P] : CompleteLattice (DM P) :=
⟨ join, join_ub, join_least ⟩
Completion Map
The mapping from P
into DM P
is defined implicitly in the construction of DM P
. Explicitly, the embedding is definition by the down
operator. That is, the map λ x ↦ down x
is the ordering embedding that wintesses the completion. To show this, we prove that down x
is closed under the lower-upper
closure operator.
theorem DM.down_is_dm {P : Type u} [Poset P] {x : P}
: lower (upper (down x)) = down x :=
by
apply Set.eq_of_subset_of_subset
. intro y hy
exact hy x fun a a ↦ a
. intro a ha
simp_all[upper,lower]
This theorem then allows us to formally define the embedded. We call it make
, because it allows us to make an element of DM P
out of any element x ∈ P
.
def DM.make {P : Type u} [Poset P] (x : P) : DM P := ⟨ down x, down_is_dm ⟩
Finally, we prove that make
is an order embeddeding (as defined in Maps).
theorem DM.make_embed {P : Type u} [Poset P]
: OrderEmbedding (make : P → DM P) := by
intro x y
constructor
. intro h z hz
exact Poset.trans z x y hz h
. intro h
simp[make,down,le_inst,Poset.le] at h
exact h x (Poset.refl x)
Completion of a Total Order
If P
is a totally ordered set, then its completion ought to be totally ordered as well. We show that here. We start with a useful theorem stating the fact that all elements of DM P
are downward closed.
theorem dm_down_close {P : Type u} [Poset P] {X : DM P}
: ∀ y, ∀ x ∈ X.val, y ≤ x → y ∈ X.val := by
intro y x hx hyx
rw[←X.h] at hx ⊢
intro z hz
apply Poset.trans y x z hyx (hx z hz)
Now we show the main result.
theorem dm_total_order {P : Type u} [TotalOrder P]
: ∀ (x y : DM P), Comparable x y := by
intro X Y
by_cases h : X.val ⊆ Y.val
. exact Or.inl h
. -- Obtain an x in X - Y
obtain ⟨ x, ⟨ hx, hxny ⟩ ⟩ := Set.not_subset.mp h
-- Show y ≤ x using the fact that Y is closed
rw[←Y.h] at hxny
simp[lower] at hxny
obtain ⟨ y, ⟨ hy, hcomp ⟩ ⟩ := hxny
have hyx : y ≤ x := by
apply Or.elim (TotalOrder.comp x y)
. intro h
exact False.elim (hcomp h)
. exact id
-- Show Y ⊆ down x using transitivity of ≤ in P
have hYdx : Y.val ⊆ down x := by
intro b hb
exact Poset.trans b y x (hy b hb) hyx
-- Show down x ⊆ X using the helper theorem above
have hdxX: down x ⊆ X.val := by
intro b hb
exact dm_down_close b x hx hb
-- Show Y ⊆ X using transitivity of ⊆
apply Or.inr
intro q hq
exact hdxX (hYdx hq)
Using this theorem, we can instantiate the total order class for DM P
for any totally ordered P
.
instance {P : Type u} [TotalOrder P] : TotalOrder (DM P) := ⟨ dm_total_order ⟩
We can show a useful theorem stating that every element besides top has a principal upper bound.
theorem DM.not_top_is_bounded {P : Type u} [Poset P] {x : DM P}
: x ≠ CompleteLattice.top → ∃ q : P, x ≤ DM.make q := by
intro h
have h1 : x.val ≠ Set.univ := by
by_contra h2
simp[CompleteLattice.top,CompleteSemilattice.inf,DM.meet,DM.inter,DM.ext] at h
apply h (DM.ext h2)
have ⟨ q, hq ⟩ : ∃ q, q ∈ Set.univ \ x.val := by
by_contra h
simp at h
exact h1 (Set.eq_univ_iff_forall.mpr h)
have h2 : ¬q ∈ x.val := by exact Set.not_mem_of_mem_diff hq
rw[←x.h] at h2
simp[upper,lower] at h2
obtain ⟨ r, ⟨ hr, hrq ⟩ ⟩ := h2
use r
simp[DM.make,down,le_inst,Poset.le]
intro y hy
simp
exact hr y hy
We can also show that every element besides bot
is non-empty.
theorem DM.not_bot_to_exists {P : Type u} [Poset P] {x : DM P}
: x ≠ CompleteLattice.bot → ∃ v, v ∈ x.val := by
intro h
apply Set.nonempty_iff_ne_empty.mpr
intro hxb
simp[CompleteLattice.bot,CompleteLattice.sup,DM.join,DM.inter,DM.union] at h
have : {x | ∀ (T : DM P), x ∈ T.val} = ∅ := by
ext q
constructor
. simp
by_contra hn
simp at hn
have := hn x
simp_all[Set.mem_empty_iff_false]
. simp
simp[this] at h
exact h (DM.ext hxb)
Examples
A Finite Example
namespace Temp
inductive MyPoset where
| a : MyPoset
| b : MyPoset
open MyPoset
def myle (x y : MyPoset) := x = y
instance : Poset MyPoset :=
⟨ myle, by simp[myle], by simp[myle], by simp[myle] ⟩
theorem my_poset_all {x : MyPoset} : x ∈ ({a, b}: Set MyPoset) := by
match x with
| a => exact Set.mem_insert a {b}
| b => exact Set.mem_insert_of_mem a rfl
def top : DM MyPoset := ⟨
{ a, b },
by
apply Set.eq_of_subset_of_subset
. intro x h
exact my_poset_all
. intro x hx
simp[lower,upper]
intro y h1 h2
match x with
| a => exact h1
| b => exact h2
⟩
def bot : DM MyPoset := ⟨
∅,
by
apply Set.eq_of_subset_of_subset
. intro x hx
simp[lower,upper] at hx
have h1 := hx a
have h2 := hx b
rw[h1] at h2
apply noConfusion h2
. exact Set.empty_subset (lower (upper ∅))
⟩
example : ∃ b : DM MyPoset, ∀ x, b ≤ x := by
use bot
intro S y hy
exact False.elim hy
end Temp
Exercises
- Show
DM ℕ
is isomorphic toℕ ∪ {∞}
wherex ≤ ∞
for allx
.
Copyright © 2025 Eric Klavins
UNDER CONSTRUCTION
Code for this chapter
Useful Theorems
theorem not_empty_to_exists {A : Type u} {S : Set A} : S ≠ ∅ → ∃ x, x ∈ S := by
intro h
by_contra h'
simp at h'
exact h (Set.eq_empty_iff_forall_not_mem.mpr h')
theorem not_full_to_not_exists {A : Type u} {S : Set A}
: S ≠ Set.univ → ∃ x, x ∉ S := by
intro h
exact (Set.ne_univ_iff_exists_not_mem S).mp h
theorem not_empty_set_diff {A : Type u} {X Y : Set A} (h : ¬X ⊆ Y)
: X \ Y ≠ ∅ := by
simp[Set.instSDiff,Set.diff]
by_contra hx
simp at hx
exact h hx
theorem remove_set_notation {T : Type*} (A : Set T) (f : T → Prop)
: { x | f x } = A ↔ ∀ x, x ∈ A ↔ f x := by
constructor
. exact fun a x ↦ Iff.symm (Eq.to_iff (congrFun a x))
. exact fun a ↦ Eq.symm (Set.ext a)
Copyright © 2025 Eric Klavins