import game.limits.seq_convCauchy

namespace xena -- hide

notation `|` x `|` := abs x -- hide

/-
Another useful result for working with sequences -- a convergent sequence is bounded.
-/

/-
We define the concepts of a bounded sequence, in much the same way
that we defined the concepts of a limit and a convergent sequence.
-/

/-
NOTE: limits/seq_cauchyBdd.lean introduces the definition

is_bdd (a : ℕ → ℝ) := ∃ B > 0, ∀ n, |a n| ≤ B

Here I omit the B > 0 condition (which follows from properties of abs?)
But this level is perhaps superfluous anyway.
-/

definition is_bound (a : ℕ → ℝ) (M : ℝ) := ∀ n, | a n | ≤ M
definition is_bounded (a : ℕ → ℝ) := ∃ M, is_bound a M

/- Lemma
A convergent sequence is bounded.
-/
open finset
lemma bounded_if_convergent (a : ℕ → ℝ)
    (ha : is_convergent a ) :
    is_bounded a :=
begin

cases ha with α islim,
specialize islim 1 (by linarith),  -- specialise to the case ε = 1, linarith knows this is OK
cases islim with N_k HN_k,         -- introduce the natural N_k fulfilling ε = 1 case

   -- We use these to show that there is a bound for all terms with index ≥ N_k
   -- Then take the max of this bound and the earlier terms.

have bounds_geq_N_k : ∀ n ≥ N_k, |a n| ≤ | α | + 1,

{
intros bigN Nisbig,                 -- introduce some bigN ≥ N_k
specialize HN_k bigN,               -- specialize to bigN
have fact1:= HN_k(Nisbig),          -- just applying our conditional
rw abs_lt at fact1,

rw abs_le,     -- note our abs_le will require us to show c ≥ 0
simp,
split,

    {have smallfact: - α ≤ | α | := neg_le_abs_self α,
        linarith},

    {have smallfact2: α ≤ | α | := le_abs_self(α),
        linarith},

-- condition for abs_le
have fact: 0 ≤ |α|, from abs_nonneg α,
linarith,

},

   --  Having shown that we have a bound for the elements above N_k, we consider the earlier terms
   --  This section is mostly cribbed from Kevin's M1P1 limits file

   let rangeset := finset.image (abs ∘ a) (range (N_k + 1)),    -- range (N) is set of nats < N

   have fact4 : |a 0| ∈ rangeset := finset.mem_image_of_mem _ (mem_range.2 (nat.zero_lt_succ _)),
   have fact5 : rangeset ≠ ∅ := ne_empty_of_mem fact4,
   have fact6 := nonempty_iff_ne_empty.mpr fact5,

   let Bx := rangeset.max' fact6,
   let B := max Bx ( |α|  + 1),

use B,
intro n,
cases lt_or_ge n N_k with Hlt Hge,

    simp,
    left,
    have HBx : ∀ n < N_k, |a n| ≤ Bx := λ n Hn, le_max' rangeset fact6 _
    (mem_image_of_mem _ (mem_range.2 (nat.lt_succ_of_lt Hn))),
    apply HBx,
    exact Hlt,

    have h2 : |a n| ≤ |α| + 1 := bounds_geq_N_k n Hge,
    have h3 : |α| + 1 ≤ B := le_max_right _ _,
    linarith,
end

end xena -- hide


/-
Intuitively, if a proposition P is true and a
second proposition Q is true, then the more
complex proposition, "P and Q" is also true.
This proposition is written symbolically as
P ∧ Q. In mathematical logic, we say that
the proposition, P ∧ Q, is the conjunction
of the individual propositions, P and Q.

There's an inference rule for that. It says
that if P and Q are propositions (types of
type Prop), and if you have a proof of P and
you have a proof of Q then you can derive a
proof of P ∧ Q. Moreover, the form of that
proof is basically the pair of proofs, (p, q).

{P Q : Prop }, p : P, q : Q
--------------------------- (∧-intro)
        (p, q): P ∧ Q

To give us some propositions and proofs to
use in examples, let's define P and Q to be
simple equality propositions, and let's then
define p and q to be proofs of P and Q.
-/

def P : Prop := 0 = 0
def Q := 1 = 1 -- type inference works here

theorem pfP : P := eq.refl 0
theorem pfQ : Q := eq.refl 1

/-
We note that rfl doesn't work here. It's
the right idea, but the proposition, P,
to be proved isn't written as an equality,
so rfl doesn't know what to do with it.
We just just eq.refl directly instead.
-/

/- * SYNTAX OF CONJUNCTIONS * -/

/--/
Now the syntax of proposition logic allows
us to form a new proposition, P ∧ Q.
-/

def P_and_Q : Prop := P ∧ Q

#check P_and_Q -- it's a proposition
#check P ∧ Q   -- same thing

/- * SEMANTICS OF CONJUNCTIONS * -/

/- * AND INTRODUCTION RULE * -/

/-
Now intuitively, P ∧ Q should be true if
and only if P is true and Q is true, which
in the logic of Lean is to say if we have
a proof of P and a proof of Q. The and.intro
inference rule formalizes this idea. It says
if you give me propositions, P and Q, and
proofs, p and q of P and Q respectively,
then I will give you back a proof of P ∧ Q.

  { P, Q : Prop } (p: P) (q: Q)
  ----------------------------- and.intro
          pf_PQ : P ∧ Q

The and.intro takes two explicit arguments: a
proof of P and a proof of Q. It infers P and Q from pf_P and pf_Q. It then derives a proof of
P ∧ Q.

Here is an example of its use in Lean
-/

theorem pf_PQ: P ∧ Q :=
    and.intro pfP pfQ -- study this carefully!

/-
Ok, let's decode that. We're proposing a theorem.
The proposition we aim to prove is P ∧ Q,
which is really just 0 = 0 ∧ 1 = 1. To produce
a proof of this proposition, we need a proof
of 0 = 0. That is p. We also need a proof of
1 = 1. That is q. If we then apply the rule to
p and q we can expect to get back a proof of
P ∧ Q,=. The Lean checker confirms that we do.

You might be getting the sense that these
inference rules are sort of like functions:
They take propositions, proofs, and other
values as arguments and they compute and
return proofs as results. This is exactly
what is happening here. What you're seeing
is how programs like Lean turns propositions
and proofs into objects that can be handled
and transformed by programs! It's like your
ordinary discrete math class on steroids.
-/

/- * PROOF TREES * -/

/-
A key point is that what we just did is to
build a larger proof, pf, from two smaller
ones, p and q. Those proofs in turn we built
using the eq.refl inference rule applied to
the values 0 and 1, respectively. We can now
visualize the whole construction as what we
call a proof tree, or derivation (with more
than one level of rules being applied).

eq.refl 0    eq.refl 1
---------    ---------
  0 = 0        1 = 1
----------------------
     0 = 0 ∧ 1 = 1

We can read such a proof tree either from
the top to the bottom or from the bottom to
the top. We can also translate such a tree
into English. Most mathematicians would just
provide the English language proof, leaving
it to human and social processes to check
that it's good. We now have the benefit of
automated proof checking.

Reading it from the top we might say the
following. By the reflexive property of
equality, we derive that 0 = 0 and that
1 = 1. These lemmas both now been proved
separately, a proof of their conjunction
follows immediately.
QED

Now as one seeking to prove P ∧ Q (the
conclusion), one would work from bottom
to top. The reasoning would go like this:
To prove the conclusion, we need proofs
of ach conjunct. We obtain a proof of the
first, 0 = 0, by noting that this follows
from the reflexive property of equality.
So does a proof of 1 = 1. Now having
proved the individual conjuncts, the truth
of the conjunction follows immediately.
QED.

You can see that the everyday working
mathematician elides many details, such
as the precise principle used to derive
the truth of the conjunction. The lack
of detail in informal proofs makes them
at best nearly impossible for machines
to check. Reading and checking them
really relies on human mathematical
understanding.

Nevertheless, whether you are asked to
write an informal proof or a formal one,
the process of deriving a proof often
involves this kind of "backwards chaining."

When asked to prove a proposition, X, we
generally try to find other propositions,
such as S and T, such that if we had
proofs of S and T, we could then apply
a valid reasoning step (inference rule)
to derive a proof of X.

One thus reduces the problem of proving
X to the sub-problems of proving S and T.
One  then backward-chains this way until
one finally reaches ... axioms, i.e.,
rules with no proofs required above the
line. Axioms are the "base cases" in
the recursive decomposition of the
overall problem: there is no need
to recurse any further once you've
reach the bottom!

To be really precise about it, there
is one more layer in the proof tree:
it's where the Lean type checker gives
us proofs that 0 and 1 are of type nat;
and that's what eq.refl needs as its
input. The type checker in turn gives
us proofs of these type judgments by
means of yet further, but not hidden,
logic reasoning.

 0: nat       1 : nat
---------    ---------
eq.refl 0    eq.refl 1
---------    ---------
  0 = 0        1 = 1
----------------------
     0 = 0 ∧ 1 = 1

QED!

Now you're looking at a depiction
of a mathematically precise proof.
The English just presents such an
object in a more natural language,
but at the cost of a massive loss
of precision and checkability.  The
downside of the formal proof object
is that it's not in the form of a
story; and as you can imagine, in
cases where proofs get massive,
understanding why they are the way
they are can be near impossible.
But that's why we have machines.

In practice, you should really
know how to produce both formal
and informal proofs. Better yet,
you might someday try some tools
that produce formal proofs for
you. (Such a tool is Dafny. We
will perhaps see it later.)
-/

/-
EXERCISE: What "smaller" propositions
might you want to prove to prove that
5 = 1 + 4 ∧ "Strike" = "S" ++ "trike".
Prove those smaller propositions giving
them whatever names you choose. Then
write the theorem in Lean that proves
the final result using and.intro.
-/

/- * Proving conjunctions with tactics * -/

/-
One can always prove a proposition by
giving an exact proof term, but these
terms are like programs, and they can
get large and complicated. Sometimes,
indeed often, it's easier to develop
them step by step.

A key idea is that we start with the
goal at the bottom of a proof tree, the
goal (proposition) to be proved, and by
working backwards through the tree, we
can identify the smaller propositions
for which we need proofs so as to be
able to apply a final rule to construct
the desired proof.

Consider again the goal of proving the
conjunctive proposition P ∧ Q. If we have
a proof of P and a proof of Q then we
can apply the and.intro rule to build
the proof we need. Well, that breaks the
overall goal into two subgoals, and we
can then solve them recursively (i.e.,
as sub-problems to solve nested within
the solution to the overall problem).

In particular, we can *split* the goal
of proving a conjunction into sub-goals,
one for each conjunct. We then solve
the sub-problems and finally combine
the partial solutions into a final result
using and.intro.

Whether you are using a tool like Lean
or are just writing informal mathematical
proofs, the use of decomposition of a
large problem into pieces, recursively
solving of the parts, and ultimately
the combination of the partial results
into a final result, is essential and
ubiquitous. Indeed, it's a general way
to solve complex problems in almost
any domain. The key is being able to
break a problem into parts that can
be solved independently.
-/

/-
In Lean, we can build proofs this way
using tactic scripts. First we apply
the and.intro rule. Used in a script in
this way, it works backwards! Rather
than combining proofs for P and Q into
a proof of P ∧ Q, it decomposes the
top-level goal of proving P ∧ Q into
two sub-goals: to prove P and to prove
Q. The problem of proving P ∧ Q is
thus decomposed into the problem of
proving premises needed to make the
and.intro rule succeed. We then use
additional tactics too solve each of
the sub-problems, one by one.

Open the Messages view, then put your
cursor at the beginning of each line in
the following script and study how the
tactic state changes as you go.
-/

theorem pf_PQ': P ∧ Q :=
begin
  apply and.intro,
  exact pfP,
  exact pfQ,
end

/-
An English-language rendition of this
proof-constructing procedure would go
like this: "We split the problem of
proving P ∧ Q into the sub-problems,
of proving P and proving Q. This is
a valid step because the and.intro
rule tells us that having such proofs
will suffice to prove the main goal.
A proof of P follows from the reflexive
property of equality, and a proof of Q
also then follows similarly. QED."

Constructing proofs in this way is
very much like writing programs by
first writing the Main routine, having
it call subroutines, and then writing
the subroutines. Here instead we start
with the main goal, P ∧ Q. We apply
an inference rule to break down the
problem of proving the goal into one
or more problems of proving sub-goals,
and once we're done with that, a proof
of the top-level goals follows by the
application of the mai inference rule.

We call this approach to development
of programs, and of proofs, top-down
decomposition. Sometimes a software
will say that she's using "top-down
structured programming" to design her
code. Learning to think this way is
a major step forward for a computer
scientist.
-/

/-
There's another way, of course, to
build a proof, or a program, and that
iss to write the sub-routines first and
then to integrate them into a larger
program by writing code that uses them.
We call such an approach "bottom-up."

You can similarly develop proofs using
a bottom-up approach. You first obtain
the proofs of sub-goals that you'll need
and then you "integrate" (combine) them
by applying some inference rule.

We now introduce a style of Lean proof
scripts that supports such a bottom-up
approach to constructing proofs. Here's
an example where we prove the same
proposition but in a bottom-up manner.
-/

theorem pf_PQ'': P ∧ Q :=
begin
  have X := pfP,
  have Y := pfQ,
  apply and.intro X Y
end

/-
Study very carefully how the tactic state
changes as you advance through the script.
The first tactic we use is "have." It
gives a name to a proof for a sub-goal
that you will then use later on. Here, the
name X is given to a proof of 0 = 0. Once
this tactic has run, you will see the proof state update to a new state in which you
have a proof X of 0 = 0 (before the ⊢),
with P ∧ Q as desired proposition to be
proved. Next we give a name to, and then
subsequently also have a proof of Q. And
finally, we apply the and.intro rule to
these two proofs, which are now available
in the "context" of the proof state (before
the ⊢) to build a proof of the overall goal. Lean checks and accepts that proof thereby
solving the overall problem. QED, as they
say.

This style of proof scripts starts to
look like proofs that a mathematician
might write. This one could be read
like this: "We have a proof of P,
(eq.refl 0). Let's call it X. We
also have a proof of Q, (eq.refl 1).
Let's call it Y. Given proofs X and Y
we now apply the and.intro rule to
X and Y to derive the proof we need.
QED."
-/

/-
Whether you write exact proof objects,
or use top-down or bottom-up scripting
methods, the key point to bear in mind
is that everything ultimately relies
on the inference rules. They are what
is most fundamental. Moving from the
conclusion to the premises of and.intro
is how we decompose the top-level goal,
P ∧ Q, into sub-goals. Moving from the
premises X and Y down through the
and.intro rule to a proof of P ∧ Q is
how we merge bottom-up partial results
into an overall solution. And of course
if we're going to write a proof object
exactly, the expression we need is one
in which eq.refl is used. It's thus very
important that you familiarize yourself
deeply with the introduction (and also
the elimination) inference rules for
predicate logic. Now we move on to the
elimination rules for and (∧).
-/

/- * AND ELIMINATION * -/

/-
Given a proof of P ∧ Q, it's clear that
P should be true individually, and Q too.

The and elimination rules provide us with
these reasoning principles, allowing us to
derive P from P ∧ Q, and Q from P ∧ Q. We
call them and.elim_left and and.elim_right.

{ P Q: Prop } (paq : P ∧ Q)
--------------------------- and.elim_left
         pfP : P

{ P Q: Prop } (paq : P ∧ Q)
--------------------------- and.elim_right
         pfQ : Q
-/

-- recall that t is a proof of P ∧ Q
#check and.elim_left pf_PQ
#check and.elim_right pf_PQ

/-
EXERCISE: Fill in the "sorry" words in
the following incomplete theorems with
explicit proof objects obtained by applying
the and elimination rules to our proof, pf_PQ,
of P ∧ Q.

Note: Sorry says, "please just accept
the theorem as proved: an axiom. It's
dangerous to use sorry  except as a
temporary placeholder for a proof that
you plan fill in later."
-/

theorem pfP' : P := and.elim_left pf_PQ
theorem pfQ' : Q := sorry

/-
Here are two script-based proofs.
-/

theorem pfP'' : P :=
begin
  exact and.elim_left pf_PQ
end

theorem pfP''' : P :=
begin
  have pfP := and.elim_left pf_PQ,
  exact pfP
end

/- * COMMUTATIVITY OF CONJUNCTION *-/

/-
Now we've really got some power tools. We
can prove equalities. From constituent
proofs we can prove conjunctions, and
from proofs of conjunctions we can obtain
proofs of the individual conjuncts. With
these tools in hand we can now start to
state and prove essential mathematical
properties of logical operators, here of
conjunction (and, ∧) .
-/

/- *** Commutativity of ∧ ***-/
/-
Let's first state a claim informally. If
P ∧ Q is true then Q ∧ P must be true
as well. For example, from a proof of
(0 = 0 ∧ 1 = 1) we should be able to
derive a proof of (1 = 1 ∧ 0 = 0).

We already have the propositions, P :=
0 = 0, Q := 1 = 1, and a proof, pf_PQ,
of P ∧ Q (i.e., of 0 = 0 ∧ 1 =1 ). Now
let's give a name to the proposition
that (1 = 1 ∧ 0 = 0).

-/

def Q_and_P : Prop := Q ∧ P -- reverse!

/-
How can we generate a proof of Q_and_P?
Let's think in a top-down manner. Start
with the goal, Q ∧ P, and ask how it can
be proved. The only way we can prove it
at present is by applying and.intro to a
proof of Q and a proof of P (in this new
and reversed order!).

So the form of a proof simply has to
be (and.intro _ _), where the first _
is a proof of Q and the second _ is a
proof of P.

If all we start with is a proof, pf_PQ,
of P ∧ Q, how can get the proofs, pfP of
P and pfQ of Q, that we need?

Easy! Apply the and.elimination rules
to get first a proof of Q then a proof
of P; then use those proofs as arguments
to and.intro, the first being the proof
of Q and the second, the proof of P.
-/

theorem pfQaP : Q ∧ P :=
  and.intro
    (and.elim_right pf_PQ)
    (and.elim_left pf_PQ)

/-
EXERCISE: Write a top-down proof script
to prove the same proposition, calling
the result pfQaP'.

EXERCISE: Write a bottom-up proof script
to prove the same proposition, calling
the result pfQaP''.
-/

theorem pfQaP' : Q ∧ P :=
begin
  apply and.intro,
  exact (and.elim_right pf_PQ),
  exact (and.elim_left pf_PQ),
end

theorem pfQaP'' : Q ∧ P :=
begin
  have pfQ := (and.elim_right pf_PQ),
  have pfP := (and.elim_left pf_PQ),
  apply and.intro pfQ pfP
end

/-
The general inference rule, which we
will call and.commutes, says that if
P and Q are *any* propositions, and
we have a proof, paq, of P ∧ Q, then
we can obtain a proof, qap, of Q ∧ P.
We could write the rule like this
(the arguments P and Q are implicit
and are inferred from paq).

{ P Q: Prop }, paq : P ∧ Q
-------------------------- and_commutes
        qap: Q ∧ P

We can now even see how we might
implement this rule ourselves --
literally as a program! We'll call
it "and_commutes".
-/

def and_commutes { P Q: Prop } (paq: P ∧ Q) : Q ∧ P :=
  and.intro (and.elim_right paq) (and.elim_left paq)

#check and_commutes

/-
In this program we use the same curly
brace notation as introduced informally
above to indicate parameters that are to
be inferred from context. That is, we can
apply this function to a proof of P ∧ Q,
without explicitly giving P and Q as
parameters, to obtain a proof of Q ∧ P.
Does it work?
-/

theorem qap : Q ∧ P := (and_commutes pf_PQ)

/-
Holy cow, it works! We applied our new
*proof converter* to convert a proof
of 0 = 0 ∧ 1 = 1 into a proof of 1 = 1 ∧
0 = 0. But we've written our converter
program in a way that will work for any
P and Q are. We have in effect proven
that conjunction is commutative: that
for any propositions, P and Q, if P
∧ Q is true, then we can derive that
Q ∧ P is, too. Now things are getting
interesting.
-/

/-
EXERCISE: From a proof of 1 = 1 ∧ tt = tt,
use and_commutes to prove tt = tt ∧ 1 = 1.
-/

/-
Having now proved that "and commutes,"
as mathematicians would say, we can now
use this principle in all future reasoning.
Indeed in the work of everyday mathematics,
if one had proved P ∧ Q and needed a proof
of Q ∧ P, it'd be a simple matter of saying
"Q ∧ P follows from the commutativity of
conjunction." Many mathematicians would
just say, "Q ∧ P follows immediately,"
leaving it to the reader to know that
the commutativity property of ∧ is the
real justification for the conclusion.
-/


import data.nat.prime
import algebra.big_operators
import tactic

/- TEXT:
.. _section_induction_and_recursion:

Induction and Recursion
-----------------------

The set of natural numbers :math:`\mathbb{N} = \{ 0, 1, 2, \ldots \}`
is not only fundamentally important in its own right,
but also a plays a central role in the construction of new mathematical objects.
Lean's foundation allows us to declare *inductive types*,
which are types generated inductively by a given list of
*constructors*.
In Lean, the natural numbers are declared as follows.
OMIT: -/
namespace hidden
-- QUOTE:
inductive nat
| zero : nat
| succ (n : nat) : nat
-- QUOTE.
end hidden

/- TEXT:
You can find this in the library by writting ``#check nat`` and
then using ``ctrl-click`` on the identifier ``nat``.
The command specifies that ``nat`` is the datatype generated
freely and inductively by the two constructors ``zero : nat`` and
``succ : nat → nat``.
Of course, the library introduces notation ``ℕ`` and ``0`` for
``nat`` and ``zero`` respectively. (Numerals are translated to binary
representations, but we don't have to worry about the details of that now.)

What "freely" means for the working mathematician is that the type
``nat`` has an element ``zero`` and an injective successor function
``succ`` whose image does not include ``zero``.
EXAMPLES: -/
-- QUOTE:
example (n : nat) : n.succ ≠ nat.zero := nat.succ_ne_zero n

example (m n : nat) (h : m.succ = n.succ) : m = n := nat.succ.inj h
-- QUOTE.

/- TEXT:
What the word "inductively" means for the working mathematician is that
the natural numbers comes with a principle of proof by induction
and a principle of definition by recursion.
This section will show you how to use these.

Here is an example of a recursive definition of the factorial
function.
BOTH: -/
-- QUOTE:
def fac : ℕ → ℕ
| 0       := 1
| (n + 1) := (n + 1) * fac n
-- QUOTE.

/- TEXT:
The syntax takes some getting used to.
Notice that there is no ``:=`` on the first line.
The next two lines provide the base case and inductive step
for a recursive definition.
These equations hold definitionally, but they can also
be used manually by giving the name ``fac`` to ``simp`` or ``rw``.
EXAMPLES: -/
-- QUOTE:
example : fac 0 = 1 := rfl
example : fac 0 = 1 := by rw fac
example : fac 0 = 1 := by simp [fac]

example (n : ℕ) : fac (n + 1) = (n + 1) * fac n := rfl
example (n : ℕ) : fac (n + 1) = (n + 1) * fac n := by rw fac
example (n : ℕ) : fac (n + 1) = (n + 1) * fac n := by simp [fac]
-- QUOTE.

/- TEXT:
The factorial function is actually already defined in mathlib as
``nat.factorial``. Once again, you can jump to it by typing
``#check nat.factorial`` and using ``ctrl-click.``
For illustrative purposes, we will continue using ``fac`` in the examples.
The annotation ``@[simp]`` before the definition
of ``nat.factorial`` specifies that
the defining equation should be added to the database of identities
that the simplifier uses by default.

The principle of induction says that we can prove a general statement
about the natural numbers by proving that the statement holds of 0
and that whenever it holds of a natural number :math:`n`,
it also holds of :math:`n + 1`.
The line ``induction n with n ih`` in the proof
below therefore results in two goals:
in the first we need to prove ``0 < fac 0``,
and in the second we have the added assumption ``ih : 0 < fac n``
and a required to prove ``0 < fac (n + 1)``.
The phrase ``with n ih`` serves to name the variable and
the assumption for the inductive hypothesis,
and you can choose whatever names you want for them.
EXAMPLES: -/
-- QUOTE:
theorem fac_pos (n : ℕ) : 0 < fac n :=
begin
  induction n with n ih,
  { rw fac, exact zero_lt_one },
  rw fac,
  exact mul_pos n.succ_pos ih,
end
-- QUOTE.

/- TEXT:
The ``induction`` tactic is smart enough to include hypotheses
that depend on the induction variable as part of the
induction hypothesis.
Step through the next example to see what is going on.
EXAMPLES: -/
-- QUOTE:
theorem dvd_fac {i n : ℕ} (ipos : 0 < i) (ile : i ≤ n) : i ∣ fac n :=
begin
  induction n with n ih,
  { exact absurd ipos (not_lt_of_ge ile) },
  rw fac,
  cases nat.of_le_succ ile with h h,
  { apply dvd_mul_of_dvd_right (ih h) },
  rw h,
  apply dvd_mul_right
end
-- QUOTE.

/- TEXT:
The following example provides a crude lower bound for the factorial
function.
It turns out to be easier to start with a proof by cases,
so that the remainder of the proof starts with the case
:math:`n = 1`.
See if you can complete the argument with a proof by induction.
BOTH: -/
-- QUOTE:
theorem pow_two_le_fac (n : ℕ) : 2^(n-1) ≤ fac n :=
begin
  cases n with n,
  { simp [fac] },
/- EXAMPLES:
  sorry
SOLUTIONS: -/
  induction n with n ih,
  { simp [fac] },
  simp at *,
  rw [pow_succ, fac],
  apply nat.mul_le_mul _ ih,
  repeat { apply nat.succ_le_succ },
  apply zero_le
-- BOTH:
end
-- QUOTE.

/- TEXT:
Induction is often used to prove identities involving finite sums and
products.
Mathlib defines the expressions ``finset.sum s f`` where
``s : finset α`` if a finite set of elements of the type ``α`` and
``f`` is a function defined on ``α``.
The codomain of ``f`` can be any type that supports a commutative,
associative addition operation with a zero element.
If you import ``algebra.big_operators`` and issue the command
``open_locale big_operators``, you can use the more suggestive notation
``∑ x in s, f x``. Of course, there is are an analogous operation and
notation for finite products.

We will talk about the ``finset`` type and the operations
it supports in the next section, and again in a later chapter.
For now, we will only make use
of ``finset.range n``, which is the finite set of natural numbers
less than ``n``.
BOTH: -/
section

-- QUOTE:
variables {α : Type*} (s : finset ℕ) (f : ℕ → ℕ) (n : ℕ)

-- EXAMPLES:
#check finset.sum s f
#check finset.prod s f

-- BOTH:
open_locale big_operators
open finset

-- EXAMPLES:
example : s.sum f = ∑ x in s, f x := rfl
example : s.prod f = ∏ x in s, f x := rfl

example : (range n).sum f = ∑ x in range n, f x := rfl
example : (range n).prod f = ∏ x in range n, f x := rfl
-- QUOTE.

/- TEXT:
The facts ``finset.sum_range_zero`` and ``finset.sum_range_succ``
provide a recursive description summation up to :math:`n`,
and similarly for products.
EXAMPLES: -/
-- QUOTE:
example (f : ℕ → ℕ) : ∑ x in range 0, f x = 0 :=
finset.sum_range_zero f

example (f : ℕ → ℕ) (n : ℕ): ∑ x in range n.succ, f x = (∑ x in range n, f x) + f n :=
finset.sum_range_succ f n

example (f : ℕ → ℕ) : ∏ x in range 0, f x = 1 :=
finset.prod_range_zero f

example (f : ℕ → ℕ) (n : ℕ): ∏ x in range n.succ, f x = (∏ x in range n, f x) * f n :=
finset.prod_range_succ f n
-- QUOTE.

/- TEXT:
The first identity in each pair holds definitionally, which is to say,
you can replace the proofs by ``rfl``.

The following expresses the factorial function that we defined as a product.
EXAMPLES: -/
-- QUOTE:

example (n : ℕ) : fac n = ∏ i in range n, (i + 1) :=
begin
  induction n with n ih,
  { simp [fac] },
  simp [fac, ih, prod_range_succ, mul_comm]
end
-- QUOTE.

/- TEXT:
The fact that we include ``mul_comm`` as a simplification rule deserves
comment.
It should seem dangerous to simplify with the identity ``x * y = y * x``,
which would ordinarily loop indefinitely.
Lean's simplifier is smart enough to recognize that, and applies the rule
only in the case where the resulting term has a smaller value in some
fixed but arbitrary ordering of the terms.
The following example shows that simplifying using the three rules
``mul_assoc``, ``mul_comm``, and ``mul_left_comm``
manages to identify products that are the same up to the
placement of parentheses and ordering of variables.
EXAMPLES: -/
-- QUOTE:
example (a b c d e f : ℕ) : a * ((b * c) * f * (d * e)) = d * (a * f * e) * (c * b) :=
by simp [mul_assoc, mul_comm, mul_left_comm]
-- QUOTE.

/- TEXT:
Roughly, the rules work by pushing parentheses to the right
and then re-ordering the expressions on both sides until they
both follow the same canonical order. Simplifying with these
rules, and the corresponding rules for addition, is a handy trick.

Returning to summation identities, we suggest stepping through the following proof
that the sum of the natual numbers up to an including :math:`n` is
:math:`n (n + 1) / 2`.
The first step of the proof clears the denominator.
This is generally useful when formalizing identities,
because calculations with division generally have side conditions.
(It is similarly useful to avoid using subtraction on the natural numbers when possible.)
EXAMPLES: -/
-- QUOTE:
theorem sum_id (n : ℕ) : ∑ i in range (n + 1), i = n * (n + 1) / 2 :=
begin
  symmetry, apply nat.div_eq_of_eq_mul_right (by norm_num : 0 < 2),
  induction n with n ih,
  { simp },
  rw [finset.sum_range_succ, mul_add 2, ←ih, nat.succ_eq_add_one],
  ring
end
-- QUOTE.

/- TEXT:
We encourage you to prove the analogous identity for sums of squares,
and other identities you can find on the web.
BOTH: -/
-- QUOTE:
theorem sum_sqr (n : ℕ) : ∑ i in range (n + 1), i^2 = n * (n + 1) * (2 *n + 1) / 6 :=
/- EXAMPLES:
sorry
SOLUTIONS: -/
begin
  symmetry, apply nat.div_eq_of_eq_mul_right (by norm_num : 0 < 6),
  induction n with n ih,
  { simp },
  rw [finset.sum_range_succ, mul_add 6, ←ih, nat.succ_eq_add_one],
  ring
end
-- QUOTE.
-- BOTH:

end

/- TEXT:
In Lean's core library, addition and multiplication are themselves defined
using recursive definitions,
and their fundamental properties are established using induction.
If you like thinking about foundational topics like that,
you might enjoy working through proofs
of the commutativity and associativity of multiplication and addition
and the distributivity of multiplication over addition.
You can do this on a copy of the natural numbers
following the outline below.
Notice that we can use the ``induction`` tactic with ``my_nat``;
Lean is smart enough to know to
use the relevant induction principle (which is, of course,
the same as that for ``nat``).

We start you off with the commutativity of addition.
A good rule of thumb is that because addition and multiplication
are defined by recursion on the second argument,
it is generally advantageous to do proofs by induction on a variable
that occurs in that position.
It is a bit tricky to decide which variable to use in the proof
of associativity.

It can be confusing to write things without the usual notation
for zero, one, addition, and multiplication.
We will learn how to define such notation later.
Working in the namespace ``my_nat`` means that we can write
``zero`` and ``succ`` rather than ``my_nat.zero`` and ``my_nat.succ``,
and that these interpretations of the names take precedence over
others.
Outside the namespace, the full name of the ``add`` defined below,
for example, is ``my_nat.add``.

If you find that you *really* enjoy this sort of thing, try defining
truncated subtraction and exponentiation and proving some of their
properties as well.
Remember that truncated subtraction cuts off at zero.
To define that, it is useful to define a predecessor function, ``pred``,
that subtracts one from any nonzero number and fixes zero.
The function ``pred`` can be defined by a simple instance of recursion.
BOTH: -/
-- QUOTE:
inductive my_nat
| zero : my_nat
| succ : my_nat → my_nat

namespace my_nat

def add : my_nat → my_nat → my_nat
| x zero     := x
| x (succ y) := succ (add x y)

def mul : my_nat → my_nat → my_nat
| x zero     := zero
| x (succ y) := add (mul x y) x

theorem zero_add (n : my_nat) : add zero n = n :=
begin
  induction n with n ih,
  { refl },
  rw [add, ih]
end

theorem succ_add (m n : my_nat) : add (succ m) n = succ (add m n) :=
begin
  induction n with n ih,
  { refl },
  rw [add, ih],
  refl
end

theorem add_comm (m n : my_nat) : add m n = add n m :=
begin
  induction n with n ih,
  { rw zero_add, refl },
  rw [add, succ_add, ih]
end

theorem add_assoc (m n k : my_nat) : add (add m n) k = add m (add n k) :=
/- EXAMPLES:
sorry
SOLUTIONS: -/
begin
  induction k with k ih,
  { refl },
  rw [add, ih],
  refl
end
-- BOTH:

theorem mul_add  (m n k : my_nat) : mul m (add n k) = add (mul m n) (mul m k) :=
/- EXAMPLES:
sorry
SOLUTIONS: -/
begin
  induction k with k ih,
  { refl },
  rw [add, mul, mul, ih, add_assoc]
end
-- BOTH:

theorem zero_mul (n : my_nat) : mul zero n = zero :=
/- EXAMPLES:
sorry
SOLUTIONS: -/
begin
  induction n with n ih,
  { refl },
  rw [mul, ih],
  refl
end
-- BOTH:

theorem succ_mul (m n : my_nat) : mul (succ m) n = add (mul m n) n :=
/- EXAMPLES:
sorry
SOLUTIONS: -/
begin
  induction n with n ih,
  { refl },
  rw [mul, mul, ih, add_assoc, add_assoc, add_comm n, succ_add],
  refl
end
-- BOTH:

theorem mul_comm (m n : my_nat) : mul m n = mul n m :=
/- EXAMPLES:
sorry
SOLUTIONS: -/
begin
  induction n with n ih,
  { rw [zero_mul], refl },
  rw [mul, ih, succ_mul]
end
-- BOTH:

end my_nat
-- QUOTE.


-- ---------------------------------------------------------------------
-- Ejercicio. Realizar las siguientes acciones:
-- 1. Importar la librería data.nat.prime data.nat.parity
-- 2. Abrir el espacio de nombres nat
-- 3. Declarar s y t como variables sobre conjuntos de números naturales.
-- 4. Declarar el hecho (ssubt : s ⊆ t)
-- 5, Añadir ssubt como hipótesis de la teoría.
-- ----------------------------------------------------------------------

import data.nat.prime data.nat.parity  -- 1
open nat                               -- 2
variables (s t : set ℕ)                -- 3
variables (ssubt : s ⊆ t)              -- 4
include ssubt                          -- 5

-- ---------------------------------------------------------------------
-- Ejercicio. Demostrar que si
--    ∀ x ∈ t, ¬ even x
--    ∀ x ∈ t, prime x
-- entonces
--    ∀ x ∈ s, ¬ even x ∧ prime x
-- ----------------------------------------------------------------------

example
  (h₀ : ∀ x ∈ t, ¬ even x)
  (h₁ : ∀ x ∈ t, prime x)
  : ∀ x ∈ s, ¬ even x ∧ prime x :=
begin
  intros x xs,
  split,
  { apply h₀ x (ssubt xs) },
  { apply h₁ x (ssubt xs) },
end

-- Prueba
-- ======

/-
s t : set ℕ,
ssubt : s ⊆ t,
h₀ : ∀ (x : ℕ), x ∈ t → ¬x.even,
h₁ : ∀ (x : ℕ), x ∈ t → x.prime
⊢ ∀ (x : ℕ), x ∈ s → ¬x.even ∧ x.prime
  >> intros x xs,
x : ℕ,
xs : x ∈ s
⊢ ¬x.even ∧ x.prime
  >> split,
| ⊢ ¬x.even
|   >> { apply h₀ x (ssubt xs) },
⊢ x.prime
  >> { apply h₁ x (ssubt xs) },
no goals
-/

-- ---------------------------------------------------------------------
-- Ejercicio. Demostrar que si
--    ∃ x ∈ s, ¬ even x ∧ prime x
-- entonces
--   ∃ x ∈ t, prime x
-- ----------------------------------------------------------------------

example
  (h : ∃ x ∈ s, ¬ even x ∧ prime x)
  : ∃ x ∈ t, prime x :=
begin
  rcases h with ⟨x, xs, _, px⟩,
  use [x, ssubt xs, px],
end

-- Prueba
-- ======

/-
s t : set ℕ,
ssubt : s ⊆ t,
h : ∃ (x : ℕ) (H : x ∈ s), ¬x.even ∧ x.prime
⊢ ∃ (x : ℕ) (H : x ∈ t), x.prime
  >> rcases h with ⟨x, xs, _, px⟩,
x : ℕ,
xs : x ∈ s,
h_h_h_left : ¬x.even,
px : x.prime
⊢ ∃ (x : ℕ) (H : x ∈ t), x.prime
  >> use [x, ssubt xs, px],
no goals
-/


import data.real.basic

def S := {x : real | x*x <= 2}

-- 1ª demostración
example : (0 : ℝ) ∈ S :=
begin
  dsimp [S],
  norm_num,
end

-- 2ª demostración
example : (0 : ℝ) ∈ S :=
begin
  change (0 : ℝ) * 0 ≤ 2,
  norm_num,
end

-- 3ª demostración
example : (0 : ℝ) ∈ S :=
begin
  unfold S,
  rw set.mem_set_of_eq,
  norm_num,
end


/-
Copyright (c) 2022 Jiale Miao. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Jiale Miao, Kevin Buzzard
-/

import analysis.inner_product_space.projection

/-!
# Gram-Schmidt Orthogonalization and Orthonormalization

In this file we introduce Gram-Schmidt Orthogonalization and Orthonormalization.

The Gram-Schmidt process takes a set of vectors as input
and outputs a set of orthogonal vectors which have the same span.

## Main results

- `gram_schmidt` : the Gram-Schmidt process
- `gram_schmidt_orthogonal` :
  `gram_schmidt` produces an orthogonal system of vectors.
- `span_gram_schmidt` :
  `gram_schmidt` preserves span of vectors.
- `gram_schmidt_ne_zero` :
  If the input of the first `n + 1` vectors of `gram_schmidt` are linearly independent,
  then the output of the first `n + 1` vectors are non-zero.
- `gram_schmidt_normed` :
  the normalized `gram_schmidt` (i.e each vector in `gram_schmidt_normed` has unit length.)
- `gram_schmidt_orthornormal` :
  `gram_schmidt_normed` produces an orthornormal system of vectors.

## TODO
  Construct a version with an orthonormal basis from Gram-Schmidt process.
-/

open_locale big_operators

variables (𝕜 : Type*) {E : Type*} [is_R_or_C 𝕜] [inner_product_space 𝕜 E]

local notation `⟪`x`, `y`⟫` := @inner 𝕜 _ _ x y

/-- The Gram-Schmidt process takes a set of vectors as input
and outputs a set of orthogonal vectors which have the same span. -/
noncomputable def gram_schmidt (f : ℕ → E) : ℕ → E
| n := f n - ∑ i : fin n, orthogonal_projection (𝕜 ∙ gram_schmidt i) (f n)
using_well_founded {dec_tac := `[exact i.prop]}

/-- `gram_schmidt_def` turns the sum over `fin n` into a sum over `ℕ`. -/
lemma gram_schmidt_def (f : ℕ → E) (n : ℕ) :
  gram_schmidt 𝕜 f n = f n - ∑ i in finset.range n,
    orthogonal_projection (𝕜 ∙ gram_schmidt 𝕜 f i) (f n) :=
begin
  rw gram_schmidt,
  congr' 1,
  exact fin.sum_univ_eq_sum_range (λ i,
    (orthogonal_projection (𝕜 ∙ gram_schmidt 𝕜 f i) (f n) : E)) n,
end

lemma gram_schmidt_def' (f : ℕ → E) (n : ℕ):
  f n = gram_schmidt 𝕜 f n + ∑ i in finset.range n,
    orthogonal_projection (𝕜 ∙ gram_schmidt 𝕜 f i) (f n) :=
by simp only [gram_schmidt_def, sub_add_cancel]

@[simp] lemma gram_schmidt_zero (f : ℕ → E) :
  gram_schmidt 𝕜 f 0 = f 0 :=
by simp only [gram_schmidt, fintype.univ_of_is_empty, finset.sum_empty, sub_zero]

/-- **Gram-Schmidt Orthogonalisation**:
`gram_schmidt` produces an orthogonal system of vectors. -/
theorem gram_schmidt_orthogonal (f : ℕ → E) {a b : ℕ} (h₀ : a ≠ b) :
  ⟪gram_schmidt 𝕜 f a, gram_schmidt 𝕜 f b⟫ = 0 :=
begin
  suffices : ∀ a b : ℕ, a < b → ⟪gram_schmidt 𝕜 f a, gram_schmidt 𝕜 f b⟫ = 0,
  { cases h₀.lt_or_lt with ha hb,
    { exact this _ _ ha, },
    { rw inner_eq_zero_sym,
      exact this _ _ hb, }, },
  clear h₀ a b,
  intros a b h₀,
  induction b using nat.strong_induction_on with b ih generalizing a,
  simp only [gram_schmidt_def 𝕜 f b, inner_sub_right, inner_sum,
    orthogonal_projection_singleton, inner_smul_right],
  rw finset.sum_eq_single_of_mem a (finset.mem_range.mpr h₀),
  { by_cases h : gram_schmidt 𝕜 f a = 0,
    { simp only [h, inner_zero_left, zero_div, zero_mul, sub_zero], },
    { rw [← inner_self_eq_norm_sq_to_K, div_mul_cancel, sub_self],
      rwa [ne.def, inner_self_eq_zero], }, },
  simp_intros i hi hia only [finset.mem_range],
  simp only [mul_eq_zero, div_eq_zero_iff, inner_self_eq_zero],
  right,
  cases hia.lt_or_lt with hia₁ hia₂,
  { rw inner_eq_zero_sym,
    exact ih a h₀ i hia₁, },
  { exact ih i hi a hia₂, },
end

/-- This is another version of `gram_schmidt_orthogonal` using `pairwise` instead. -/
theorem gram_schmidt_pairwise_orthogonal (f : ℕ → E) :
  pairwise (λ a b, ⟪gram_schmidt 𝕜 f a, gram_schmidt 𝕜 f b⟫ = 0) :=
@gram_schmidt_orthogonal 𝕜 _ _ _ f

open submodule set order

/-- `gram_schmidt` preserves span of vectors. -/
lemma span_gram_schmidt (f : ℕ → E) (c : ℕ) :
  span 𝕜 (gram_schmidt 𝕜 f '' Iic c) = span 𝕜 (f '' Iic c) :=
begin
  induction c with c hc,
  { simp only [Iic, gram_schmidt_zero, le_zero_iff, set_of_eq_eq_singleton, image_singleton], },
  have h₀ : ∀ b, b ∈ finset.range c.succ → gram_schmidt 𝕜 f b ∈ span 𝕜 (f '' Iic c),
  { simp_intros b hb only [finset.mem_range, nat.succ_eq_add_one],
    rw ← hc,
    refine subset_span _,
    simp only [mem_image, mem_Iic],
    refine ⟨b, by linarith, by refl⟩, },
  rw [← nat.succ_eq_succ, Iic_succ],
  simp only [span_insert, image_insert_eq, hc],
  apply le_antisymm,
  { simp only [nat.succ_eq_succ,gram_schmidt_def 𝕜 f c.succ, orthogonal_projection_singleton,
      sup_le_iff, span_singleton_le_iff_mem, le_sup_right, and_true],
    apply submodule.sub_mem _ _ _,
    { exact mem_sup_left (mem_span_singleton_self (f c.succ)), },
    { exact submodule.sum_mem _ (λ b hb, mem_sup_right (smul_mem _ _ (h₀ b hb))), }, },
  { rw [nat.succ_eq_succ, gram_schmidt_def' 𝕜 f c.succ],
    simp only [orthogonal_projection_singleton,
      sup_le_iff, span_singleton_le_iff_mem, le_sup_right, and_true],
    apply submodule.add_mem _ _ _,
    { exact mem_sup_left (mem_span_singleton_self (gram_schmidt 𝕜 f c.succ)), },
    { exact submodule.sum_mem _ (λ b hb, mem_sup_right (smul_mem _ _ (h₀ b hb))), }, },
end

/-- If the input of the first `n + 1` vectors of `gram_schmidt` are linearly independent,
then the output of the first `n + 1` vectors are non-zero. -/
lemma gram_schmidt_ne_zero (f : ℕ → E) (n : ℕ)
  (h₀ : linear_independent 𝕜 (f ∘ (coe : fin n.succ → ℕ))) :
    gram_schmidt 𝕜 f n ≠ 0 :=
begin
  induction n with n hn,
  { intro h,
    simp only [gram_schmidt_zero, ne.def] at h,
    exact linear_independent.ne_zero 0 h₀ (by simp only [function.comp_app, fin.coe_zero, h]), },
  { by_contra h₁,
    rw nat.succ_eq_add_one at hn h₀ h₁,
    have h₂ := gram_schmidt_def' 𝕜 f n.succ,
    simp only [nat.succ_eq_add_one, h₁, orthogonal_projection_singleton, zero_add] at h₂,
    have h₃ : f (n + 1) ∈ span 𝕜 (f '' Iic n),
    { rw [h₂, ← span_gram_schmidt 𝕜 f n],
      apply submodule.sum_mem _ _,
      simp_intros a ha only [finset.mem_range],
      apply submodule.smul_mem _ _ _,
      refine subset_span _,
      simp only [mem_image, mem_Iic],
      exact ⟨a, by linarith, by refl⟩, },
    change linear_independent 𝕜 (f ∘ (coe : fin (n + 2) → ℕ)) at h₀,
    have h₄ : ((n + 1) : fin (n + 2)) ∉ (coe : fin (n + 2) → ℕ) ⁻¹' (Iic n),
    { simp only [mem_preimage, mem_Iic, not_le],
      norm_cast,
      rw fin.coe_coe_of_lt;
      linarith, },
    apply linear_independent.not_mem_span_image h₀ h₄,
    rw [image_comp, image_preimage_eq_inter_range],
    simp only [function.comp_app, subtype.range_coe_subtype],
    convert h₃,
    { norm_cast,
      refine fin.coe_coe_of_lt (by linarith), },
    { simp only [inter_eq_left_iff_subset, Iic, set_of_subset_set_of],
      exact (λ a ha, by linarith), }, },
end

/-- If the input of `gram_schmidt` is linearly independent, then the output is non-zero. -/
lemma gram_schmidt_ne_zero' (f : ℕ → E) (h₀ : linear_independent 𝕜 f) (n : ℕ) :
  gram_schmidt 𝕜 f n ≠ 0 :=
gram_schmidt_ne_zero 𝕜 f n (linear_independent.comp h₀ _ (fin.coe_injective))

/-- the normalized `gram_schmidt`
(i.e each vector in `gram_schmidt_normed` has unit length.) -/
noncomputable def gram_schmidt_normed (f : ℕ → E) (n : ℕ) : E :=
(∥gram_schmidt 𝕜 f n∥ : 𝕜)⁻¹ • (gram_schmidt 𝕜 f n)

lemma gram_schmidt_normed_unit_length (f : ℕ → E) (n : ℕ)
  (h₀ : linear_independent 𝕜 (f ∘ (coe : fin n.succ → ℕ))) :
    ∥gram_schmidt_normed 𝕜 f n∥ = 1 :=
by simp only [gram_schmidt_ne_zero 𝕜 f n h₀,
  gram_schmidt_normed, norm_smul_inv_norm, ne.def, not_false_iff]

lemma gram_schmidt_normed_unit_length' (f : ℕ → E) (n : ℕ)
  (h₀ : linear_independent 𝕜 f) :
    ∥gram_schmidt_normed 𝕜 f n∥ = 1 :=
by simp only [gram_schmidt_ne_zero' 𝕜 f h₀,
  gram_schmidt_normed, norm_smul_inv_norm, ne.def, not_false_iff]

/-- **Gram-Schmidt Orthonormalization**:
`gram_schmidt_normed` produces an orthornormal system of vectors. -/
theorem gram_schmidt_orthonormal (f : ℕ → E) (h₀ : linear_independent 𝕜 f) :
  orthonormal 𝕜 (gram_schmidt_normed 𝕜 f) :=
begin
  unfold orthonormal,
  split,
  { simp only [gram_schmidt_normed_unit_length', h₀, forall_const], },
  { intros i j hij,
    simp only [gram_schmidt_normed, inner_smul_left, inner_smul_right, is_R_or_C.conj_inv,
      is_R_or_C.conj_of_real, mul_eq_zero, inv_eq_zero, is_R_or_C.of_real_eq_zero, norm_eq_zero],
    repeat { right },
    exact gram_schmidt_orthogonal 𝕜 f hij, },
end


/-

* proposition built using ¬
* the formal meaning of ¬
* proofs of inequalities, ¬a=b
* disjointness of constructors
* strategy: proof by negation
* inference rule: modus tollens
* principle: non-contradiction
* rule: negation elimination
* axiom: excluded middle
* classical vs constructive logic
* double-negation elimination
* proof by contradiction
* proof by contrapositive
-/

/-   **********************
     *** ¬ Introduction ***
     **********************
-/

/-
In constructive logic, if we can
construct a proof of a proposition,
P, then we can judge it to be true.

What does it mean in constructive
logic for a proposition, P, to be
false?

It means that there is a proof that
there can be no proof of P. The way
we express this is that from a proof
of P we could derive a contradiction:
a proof of false. Because there can
be no such thing, there must be no
proof of P. In other words, to show
that ¬ P is true we prove P → false.
Indeed, the proposition ¬ P is just
defined as the proposition P → false.

A proof of P → false, as with any
proof of an implication, will be
given as a function: one that
takes a proof of P as an argument
and derives a proof of false. As
there is no proof of false, such
an argument cannot exist. From
the existence of the function we
conclude the non-existence of any
proof of P: that is, ¬ P.

From now on, when working in
constructive logic, when you see
a proof of a proposition, ¬ P, it
is worthwhile viewing it as a value
of type P → false, which is to say
as a function taking proofs of P
and returning proofs of false.
-/

/-
Check it: ¬ P is the same as P → false
-/
variable P : Prop -- assume P is some Prop
-- now show ¬ P = P → false from (using) rfl.
theorem same : (¬ P) = (P → false) := rfl

/-
In previous chapters we've worked with
many equality propositions. What about
inequality propositions? By x ≠ y, we
mean simply ¬ x = y. As an example,
0 ≠ 1 is just different notation for
¬ 0 = 1. Note that in Lean, = binds
more tightly (has a higher precedence)
than ¬, so ¬ 0 = 1 means ¬ (0 = 1),
rather than (¬ 0) = 1.
-/

/-
Now in turn we understand ¬ 0 = 1 to
mean 0 = 1 → false. How can we prove
such an implication? We already know it
will be with a function that derives a
proof of false from a proof of 0 = 1.
-/

/-
The short, satisfying, but ultimately
unenlightening answer is that Lean
just knows that it's true. We write
the proposition, then write a period,
and Lean does the rest! Here it is.
-/

theorem zneqo : 0 ≠ 1. -- note period
#check zneqo

theorem zneqoeqzneqo : (0 ≠ 1) = ¬(0 = 1) := rfl

/-
Note that the types, 0 ≠ 1, ¬ 0 = 1,
and 0 = 1 → false, look different but
in fact they are just different ways
of expressing the same type.
-/

/-
We' now explain in a little more
detail what's going on. How does
"Lean know" there's a proof and
how to construct it? We present
the following elaborated proof,
and then explain it after the
code. They thing to focus on is
that nat.no_confusion construct.
It in turn is using what we call
"disjointness of constructors" to
construct a proof of false.

Let's start by just looking at
the actual proof of 0 ≠ 1, which
we write here as 0 = 1 → false to
make it clear that 0 ≠ 1 is really
a proposition in the form of an
implication.
-/

#reduce nat.zero
#reduce nat.succ(0)
#reduce nat.succ(nat.succ(0))

theorem zneqo' : 0 = 1 → false :=
    λ h : (0 = 1),
        nat.no_confusion h
#check zneqo'

/-
You can see that the proof, being
a proof of an implication, is in
form of a function, as expected.

The function takes an argument,
h (a proof of 0 = 1). It then
constructs and returns a proof
of false. It constructs this
proof by applying nat.no_confusion
to h.

The nat.no_confusion function, in
turn, was generated by Lean when
the nat type was defined. It was
generated precisely to enable us
to reason about inequalities of
terms of type nat.
-/

/-
So, one major mystery remains here.
What is nat.no_confusion really doing?
The key to understanding is in a
principle we haven't discussed yet.
Let's look ahead just a little to
see what it is.
-/

/-
Values of a type can be built by
different "constructors" of that
type. In particular, 0 and 1 are
built by different constructors
of the nat type. The nat type has
two constructors: 0 is a nat, and
if n is any nat, then succ n is a
nat. In particular, 1 is actually
just succ 0 (which we can read as
"the successor of, one more than,
 0".)

The principle that nat.no_confusion
uses is that values of a type that
are built by different constructors
are, by definition, never equal. We
say that "constructors are disjoint"
as a shorthand for saying that the
sets of values generated by different
constructors are disjoint, which is
to say they have no values in common.

Lean can thus tell immediately
that it's impossible for 0 = 1
because the terms 0 and 1 were
built by different constructors.

The nat.no_confusion function
is defined to return false if
it could ever be given a proof
of such an impossibility. Our
proof of our theorem uses the
no_confusion rule/principle
that Lean provides, following
from the definition of the nat
type. From a proof of 0 = 1 we
use nat.no_confusion to derive
a proof of false, showing that
0 = 1 → false, thus ¬ 0 = 1,
and thus 0 ≠ 1.
-/

/-
The Assume-Show-From proof pattern
-/

/-
Here's a equivalent tactic script
for generating the same proof of
0 ≠ 1.
-/

theorem zneqo'' : ¬ 0 = 1 :=
    begin
        assume h : (0 = 1),
        show false,
        from nat.no_confusion h
    end

/-
This example introduces what we will
call the "assume, show, from" proof
pattern. The assume h corresponds to
h being an argument of the function
we are defining to prove ¬ 0 = 1. The
show false states what is to be proved
(the "return type"). And the from then
provides the function body, the code
that actually constructs the proof,
using what has been assumed. Be sure
to open the Messages View in VSCode,
click through the script line by line,
and watch how the tactic state changes.
-/

/- Translation to an informal proof. -/

/-
An English language rendition of this
proof would go like this. We prove
that 0 ≠ 1 by assuming 0 = 1 and by
showing that this assumption leads to
a contradiction. As that is impossible,
there must be no such proof of 0 = 1.
That proves ¬ 0 = 1, i.e., 0 ≠ 1.
-/

/-
Disjointness of Constructors in General
-/

/-
Proofs of inequalities for values of
types other than nat can be produced
using the no_confusion principles of
the respective other types. Here's a
proof that tt ≠ ff using the principle
for the bool type.
-/

theorem ttneqff' : ¬ tt = ff :=
    begin
        assume h : (tt = ff),
        show false,
        from bool.no_confusion h
    end

/-
We're thankful we can use the period
(dot) notation here as well.
-/

theorem ttneqff : tt ≠ ff.

/-
EXERCISE: Is it true that
"Hello, Lean!" ≠ "Hello Lean!"?
Can you prove it? If so, how?
If not, why not?

EXERCISE: What about 2 ≠ 1?
-/

/-  ***************************
     *** Proof by Negation ***
    ***************************
-/

/-
We've thus got our introduction rule for ¬.
To derive ¬ P, show that from an assumption
of (a proof of P) some kind of contradiction
that cannot occur, and thus a proof of false,
would follow, leading to the conclusion that
there must be no proof of P, that it isn't
true, and that ¬ P therefore is true. This is
called "proof by negation."
-/

/-
What are some of the consequence of the
the understanding of negation that we've
developed so far?

Perhaps of the most important concept
at this point is that we have a strategy
for proving propositions of the form, ¬ P.
Again, we call it "proof by negation." To
use this strategy to prove ¬ P, we first
assume that we P is true (we have a proof)
and we show this leads to a contradiction
(we can build a proof of false). That then
justifies the conclusion that P is cannot
be true, thus ¬ P.

Negation introduction, or proof by negation,
starts by assuming  P, derives a contradiction,
and concludes with ¬ P.

Here's the principle in the form of a simple
theorem that simply restates that from a
proof of P → false we can derive ¬ P. Give
the  lambda expression a careful reading:
it says, if you assume P is a proposition,
and p is a proof of (P → false) then you
can produce a proof of ¬ P, and it's just
p itself. A value (function) of type
P → false *is* a proof of ¬ P. This is the
simple inference rule for ¬ introduction,
and the "strategy" of proof by negation.
-/

theorem proof_by_negation :
    ∀ P : Prop, (P → false) →
        ¬ P :=

        λ (P: Prop) (p: P → false), p

/-
To show ¬ P, show that assuming P leads
to a contradiction. That's just what we
just did. Here's a more concrete example.
-/

lemma zneqo''': ¬ (0 = 1) :=
begin
    apply proof_by_negation,
    assume h: (0 = 1),
    show false,
    from (nat.no_confusion h)
    end

/-
A classic example of a proof by negation
is a proof that the square root of two is
irrational. You have to analyze the English
a little. This is really a proposition, P =
"the square root of two is NOT rational,
or ¬ (rational (sqrt 2)).

To prove it "by negation", assume that the
square root of two IS rational. From that,
derive a contradiction. From that, conclude
that the square root of two is not rational.
And now you have proved it is irrational.
-/

/- *********************
   *** Modus Tollens ***
   *********************
-/

/-
Aristotle's reasoning
principle, modus tollens, allows one
to deduce ¬ Q → ¬ P from P → Q. Here's
an example: if you know that "if it's
raining then the streets are wet" then
you can deduce that "if the streets are
not wet, it is not raining.
-/

/-
We first validate this rule by writing it
as a function that takes a proof of P → Q
and then it takes a proof of ¬ Q, and from
it, it derives a proof of ¬ P. This shows
that (P → Q) → (¬ Q → ¬ P).
-/

theorem  modus_tollens' { P Q : Prop }
    (pfPtoQ : P → Q) (pfnQ : ¬ Q) : ¬ P:=
        λ (pfP : P), pfnQ (pfPtoQ pfP)

/-
EXERCISE: present this same construction using
a lambda expression. This presentation style
makes the proposition, modus tollens, explicit:
for all P and Q : Prop, (P → Q) → (¬ Q → ¬ P).
Fill in the blank.
-/

theorem modus_tollens:
     ∀ P Q: Prop, (P → Q) → (¬ Q → ¬ P) :=
        λ (P Q: Prop) (pfPtoQ: P → Q),
            λ pfnQ : ¬ Q,
                λ pfP : P,
                    pfnQ (pfPtoQ pfP)
/-
theorem wrong : ∀ (P Q : Prop), (P → Q) → (¬ P → ¬ Q) :=
    λ (P Q: Prop),
        λ (p2q : P → Q),
            λ (notP : ¬ P),
                λ (pfQ : Q),
                    _
-/

def inc (n : ℕ) : ℕ := n + 1
def inc' : ℕ → ℕ := λ n : ℕ, n + 1
def inc'' : ∀ n : nat, nat :=
    λ n : nat, n + 1

/-
There are two ways to view the modus
tollens rule.

The first view is that the aim is to
produce a proof of ¬ Q → ¬ P and the
way it is done is to prove P → Q.

The other view is that the aim is to
produce a proof of ¬ P, and the way
to do it is by proving both a proof
of P → Q and a proof of ¬ Q.

In the first case, the goal is
¬ Q → ¬ P, but of course the first
step in proving this implication is
to assume ¬ Q.

So in the first approach, we assume
¬ Q by making (a value of type) ¬ Q
an argument. In the second case, we
assume ¬ Q using a lambda -- and you
guessed it, ¬ Q is an argument.

You should be sure you understand
why these two theorems say the same
things and why the proofs are really
the same, even though they're
presented a bit differently.

-/

/-
Let's use this strategy to prove ...

EXAMPLE
-/

/- ***************************
   **** Non-contradiction ****
   ***************************
-/

/-
The principle of non-contradiction says that
a proof of any proposition, Q, and also of its
negation, ¬ Q, gives rise to a contradiction,
i.e., to a proof of false. Therefore such a
contradiction cannot arise. That is, for any
proposition Q, it's the case that ¬ (Q ∧ ¬ Q).
-/

theorem no_contra :
∀ Q: Prop, ¬ (Q ∧ ¬ Q) :=
    λ (Q : Prop) (pf : Q ∧ ¬ Q),
        (and.elim_right pf) (and.elim_left pf)

#check no_contra

variables a b : nat

theorem ncab : ¬ ((a = b) ∧ (a ≠ b)) :=
begin
  apply no_contra
end

/-
What happens when we apply no_contra here
is that Lean matches up ((a = b) ∧ (a ≠ b))
with Q ∧ ¬ Q by matching up Q with (a = b),
and then deriving false from the result to
polish off the proof (false is a get out of
jail free card).
-/

/-
A longer, less clear version that does
the same thing.
-/
theorem ncab' : ¬ ((a = b) ∧ (a ≠ b)) :=
begin
  assume c : ((a = b) ∧ (a ≠ b)),
  have l := c.1,    -- short for left elim
  have r := c.2,    -- same for right elim
  have f := r l,    -- now a proof of false
  assumption        -- and that proves it
end

/-
The assumption tactic tells lean that the
goal to be proved is already proved by an
assumption in the context, so just go find
it. Watch how the tactic state changes as
you move through the proof script steps.
-/

/-  ********************************
     *** NEGATION ELIMINATION ***
    ********************************
-/

theorem dne : ∀ P : Prop, ¬ ¬ P → P :=
    λ (P : Prop) (pf : ((P → false) → false)),
      sorry

/-
What about negation elimination? Negation
elimination works in a closely related way.
Rather than deriving a contradiction from
a proof of P and concluding ¬ P, you show
that assuming ¬ P leads to a contraction,
thus to the conclusion that ¬ P is false,
there can be no proof it, thus ¬ ¬ P; and
then, by the principle of double negation
elimination one deduces that P must be true.

This is called proving P by contradiction.

Proof by contradiction is not accepted as
a valid principle in constructive logic,
because it relies on ¬ ¬ P → P. This issue
is that this rule is not constructive. To
see the problem, expand the ¬ signs into
their corresponding arrow notations. Then
¬ ¬ P, ¬ (¬ P), is ((P → false) → false).

What this says is that from a function
that converts assumed proofs of P into
proofs of false, one can derive a proof
of false: there can be no function of
this kind. But no where buried in any of
this is an actual proof of P to be found!
There's  no way to convert a function of
type ((P → false) → false) into a proof
of P, so there is no proof of ¬ ¬ P → P.

That is, double negation elimination is
not a valid inference rule in constructive
logic. Because proofs by contradiction
rely on double negation elimination, they
are not valid or available either in a
constructive logic, such as Lean's.

Lean can be extended by a single axiom,
however, to make it classical, rather
than constructive. One simple asserts
the law of the excluded middle.

P : Prop
--------
P ∨ ¬ P

axiom excluded_middle: ∀ P : Prop, P ∨ ¬ P
-/

/-  *******************************
    ** Classical Excluded Middle **
    *******************************
-/

/-
In constructive logic, a proof of ¬ P is
a proof of P → false, which we interpret
as a proof that there can be no proof of P.
A proof of ¬ ¬ P is thus a proof that there
is no proof of ¬ P. In a constructive logic,
however, knowing that there no proof of ¬ P
is not the same as having a proof of P. In
symbolic terms, from the assumption that
(P → false) → false (in one view a proof
that there can be no program that converts
proofs of P into proofs of false), you can't
derive a proof of P.
-/
axiom excluded_middle : ∀ P, P ∨ ¬ P

axiom excluded_middle' (P : Prop) : P ∨ ¬ P

#check excluded_middle

/-
The axiom of the excluded middle is thus
added to the other rules of the constructive
logic of Lean, making the logic classical.
We gain an ability to prove more theorems,
at the cost of a loss of constructiveness.
(We don't need a proof of either P or of
not P to derive a proof of P ∨ ¬ P. Thus
no proof of either P or of ¬ P can be
obtained from a proof of P ∨ ¬ P. In our
constructive logic, as we will see soon,
in our constructive logic a proof of P ∨
¬ P has to contain either a proof of P or
a proof of ¬ P. The elimination rule gets
us back to these proofs in a certain way.
-/

/-  *********************************
    ** Double Negation Elimination **
    *********************************
-/

/-
What the axiom of the excluded middle
lets you assume is that there are only
two possibilities: either P or ¬P, so
because ¬¬P is clearly not ¬P, the
only remaining possibility is that it
is P, so it must be P. Thus ¬¬P is P.
-/

theorem double_neg_elim: ∀ { P : Prop }, ¬¬P → P :=
begin
  assume P : Prop,
  assume pfNotNotP : ¬¬P,
  cases excluded_middle P,
    -- case where P is true
    show P, from h,
    -- case where ¬ P is true
    have f: false := pfNotNotP h,
    exact false.elim f,
end

theorem double_neg_elim': ∀ { P }, (P ∨ ¬P) → ¬¬P → P :=
begin
  assume P : Prop,
  assume PornotP : P ∨ ¬P,
  assume notnotP : ¬¬P,
  cases PornotP,
    show P, from PornotP,

    have f: false := notnotP PornotP,
    exact false.elim f
end

/-
The proof uses a technique that we haven't
discussed yet: by case analysis. For now it
is enough to see that the principle can be
proved, at least if one accepts excluded
middle.
-/

variable notNotP : ¬¬P -- assume ¬ ¬ P
-- derive P by double negation elimination
theorem pfP : P := double_neg_elim notNotP
theorem pfP' : P := double_neg_elim' (excluded_middle P) notNotP

/-
Note: the expression  double_neg_elim notNotP
takes advantage of the type inference that we
asked for by surrounding { P } with the curly
braces in the statement of the theorem.
-/

/-  ******************************
    *** Proof By Contradiction ***
    ******************************
-/

/-
We thus have the fundamental classical
logical "strategy" of proof by contradiction.
Read the proposition. If assuming ¬ P leads
to a contradiction, then P must be true.
The reliance on double elimination is clear.
The term ¬ P → false is the same as ¬ ¬ P.

The natural reasoning is like this: if ¬
P leads to a contraction then it must be
true that ¬ P is not true, so ¬ ¬ P must
be. But this, by classical double negation
elimination, is just P, so P must be true.

This is negation elimination in the sense
that one starts with an assumption of ¬ P
and concludes with P, albeit by way of
¬ ¬ P. A proof by contradiction aims to
prove P.
-/
theorem proof_by_contradiction :
    ∀ P : Prop, (¬ P → false) → P :=
        @double_neg_elim

theorem proof_by_contradiction' : ∀ P : Prop,
    (¬ P → false) → P :=
    begin
      assume P : Prop,
      assume nnP : ¬P → false,
      cases excluded_middle P,
        show P, from h,

        have f: false := nnP h,
        exact false.elim f
    end

/-
The @ here turns off type inferencing for
this one reference to double_neg_elim. It
is a detail here. We'll discuss @ later.
The point is that proof by contradiction
relies on double negation elimination.
-/

/-
Proving in Lean that these laws are valid
might seem unmotivated at first. But once
we have these rules, we can use them as our
own higher-level inference rules. Here's a
proof by contradiction of 0 = 0. The goal
to start is a proof of 0 = 0. By applying
our proof_by_contradiction theorem to effect
backwards (through the theorem) reasoning,
we convert the goal of showing 0 = 0 into
the goal of showing that ¬ 0 = 0 → false.
This is exactly the strategy of a proof by
contradiction. The rest of our script does
the construction of false from the assumed
proof of 0 = 0 → false. We just use eq.refl
0 to produce a proof of 0 = 0, to which we
then apply this function, yielding a false.
-/

theorem zeqz : 0 = 0 :=
begin
    apply proof_by_contradiction,
    assume pf: 0 = 0 → false,
    show false,
    from pf (eq.refl 0)
end

/-
The standard method for introducing
the law of the excluded middle, so as
to enable classical reasoning, in Lean,
is to use the "open classical" command
to make various classical axioms and
derived theorems available for use.
-/
open classical

/-
The example keyword lets us write a
theorem without giving it a name. It
is useful for giving examples!
-/
example
    { P Q : Prop }
    (pf: ¬ P → (Q ∧ ¬ Q))
    : P :=
    begin
        apply proof_by_contradiction,
        assume notP: ¬ P,
        have contra := (pf notP),
        show false,
        from
        begin
            have notQ := contra.right,
            have pfQ := contra.left,
            show false,
            from notQ pfQ
        end
    end

/-
Remember: To use proof by contradiction
we have to use the axiom (or the so-called
"law") of the excluded middle. Clearly it's
optional as a "law", as constructive logic
does without it. We thus generally call it
the *axiom* of the excluded middle. Take it
if you're classicist; leave it if you're
constructivist.
-/

/-  *****************************
    ** Proof By Contrapositive **
    *****************************
-/

/-
The aim in a proof by contrapositive
is to show P → Q from an assumption of
¬ P → ¬ Q, i.e., (¬ Q → ¬ P) → (P → Q).

Another way to view this rule is that
it aims to show Q from, first, a proof
of ¬ Q → ¬ P, and, second, a proof of P.

The way to think about this latter view
is that it says, if Q being false implies
that P is false, and if we know that P is
true, then Q must not be false and so (by
the axiom of the excluded middle) Q must
be true.
-/

theorem proof_by_contrapositive:
    ∀ P Q : Prop, (¬ Q → ¬ P) → (P → Q) :=
    begin
        assume P Q: Prop,
        assume nqnp: (¬ Q → ¬ P),
        assume p : P,
        have nqf : ¬ Q → false :=
          λ nq : ¬ Q,
            no_contra P (and.intro p (nqnp nq)),
        have nnq : ¬ ¬ Q := nqf,
        show Q,
        from double_neg_elim nnq
    end

/-
http://zimmer.csufresno.edu/~larryc/proofs/proofs.contrapositive.html
-/

theorem zeqz' : 0 = 0 → true :=
begin
    apply proof_by_contrapositive,
    assume nt : ¬true,
    have pff := nt true.intro,
    show ¬ 0 = 0,
    from false.elim pff
end

/-
Compare what by_contrapositive does to goal
with what by_contradiction does to the goal.
-/

theorem zeqz'' : 0 = 0 → true :=
begin
    apply proof_by_contradiction (0 = 0 → true),
    sorry
end
/-
EXERCISE: Does it appear that one needs
to use proof by contradiction (and thus
classical, non-constructive, reasoning)
to prove that the square root of two is
irrational?
-/


import .love06_monads_demo

/-! # LoVe Homework 6: Monads

Homework must be done individually. -/

set_option pp.beta true
set_option pp.generalized_field_notation false

namespace LoVe

/-! ## Question 1 (6 points): Better Exceptions

The __error monad__ is a monad stores either a value of type `α` or an error of
type `ε`. This corresponds to the following type: -/

inductive error (ε α : Type) : Type
| good : α → error
| bad  : ε → error

/-! The error monad generalizes the option monad seen in the lecture. The `good`
constructor, corresponding to `option.some`, stores the current result of the
computation. But instead of having a single bad state `option.none`, the error
monad has many bad states of the form `bad e`, where `e` is an "exception" of
type `ε`.

1.1 (1 point). Implement a variant of `list.nth` that returns an error
message of the form "index _i_ out of range" instead of `option.none` on
failure.

Hint: For this, you will only need pattern matching (no `do` syntax). -/

#check list.nth

def list.nth_error {α : Type} (as : list α) (i : ℕ) : error string α :=
sorry

/-! 1.2 (1 point). Complete the definitions of the `pure` and `bind` operations
on the error monad: -/

def error.pure {ε α : Type} : α → error ε α :=
sorry

def error.bind {ε α β : Type} : error ε α → (α → error ε β) → error ε β :=
sorry

/-! The following type class instance makes it possible to use `>>=` and `do`
notations in conjunction with error monads: -/

@[instance] def error.monad {ε : Type} : monad (error ε) :=
{ pure := @error.pure ε,
  bind := @error.bind ε }

/-! 1.3 (2 point). Prove the three laws for the error monad. -/

lemma error.pure_bind {ε α β : Type} (a : α) (f : α → error ε β) :
  (pure a >>= f) = f a :=
sorry

lemma error.bind_pure {ε α : Type} (ma : error ε α) :
  (ma >>= pure) = ma :=
sorry

lemma error.bind_assoc {ε α β γ : Type} (f : α → error ε β) (g : β → error ε γ)
    (ma : error ε α) :
  ((ma >>= f) >>= g) = (ma >>= (λa, f a >>= g)) :=
sorry

/-! 1.4 (1 point). Define the following two operations on the error monad.

The `throw` operation raises an exception `e`, leaving the monad in a bad state
storing `e`.

The `catch` operation can be used to recover from an earlier exception. If the
monad is currently in a bad state storing `e`, `catch` invokes some
exception-handling code (the second argument to `catch`), passing `e` as
argument; this code might in turn raise a new exception. If `catch` is applied
to a good state, nothing happens—the monad remains in the good state. As a
convenient alternative to `error.catch ma g`, Lean lets us write
`ma.catch g`. -/

def error.throw {ε α : Type} : ε → error ε α :=
sorry

def error.catch {ε α : Type} : error ε α → (ε → error ε α) → error ε α :=
sorry

/-! 1.5 (1 point). Using `list.nth_error`, the monad operations on `error`, and
the special `error.catch` operation, write a `do` program that swaps the values
at indexes `i` and `j` in the input list `as`. If either index is out of range,
return `as` unchanged. -/

def list.swap {α : Type} (as : list α) (i j : ℕ) : error string (list α) :=
sorry

#reduce list.swap [3, 1, 4, 1] 0 2   -- expected: error.good [4, 1, 3, 1]
#reduce list.swap [3, 1, 4, 1] 0 7   -- expected: error.good [3, 1, 4, 1]

/-! ## Question 2 (3 points + 1 bonus point): Properties of `mmap`

We will prove some properties of the `mmap` function introduced in the
lecture's demo. -/

#check mmap

/-! 2.1 (1 point). Prove the following identity law about `mmap` for an
arbitrary monad `m`.

Hint: You will need the lemma `lawful_monad.pure_bind` in the induction step. -/

lemma mmap_pure {m : Type → Type} [lawful_monad m] {α : Type} (as : list α) :
  mmap (@pure m _ _) as = pure as :=
sorry

/-! Commutative monads are monads for which we can reorder actions that do not
depend on each others. Formally: -/

@[class] structure comm_lawful_monad (m : Type → Type)
  extends lawful_monad m : Type 1 :=
(bind_comm {α β γ δ : Type} (ma : m α) (f : α → m β) (g : α → m γ)
     (h : α → β → γ → m δ) :
   (ma >>= (λa, f a >>= (λb, g a >>= (λc, h a b c)))) =
   (ma >>= (λa, g a >>= (λc, f a >>= (λb, h a b c)))))

/-! 2.2 (1 point). Prove that `option` is a commutative monad. -/

lemma option.bind_comm {α β γ δ : Type} (ma : option α) (f : α → option β)
    (g : α → option γ) (h : α → β → γ → option δ) :
  (ma >>= λa, f a >>= λb, g a >>= λc, h a b c) =
  (ma >>= λa, g a >>= λc, f a >>= λb, h a b c) :=
sorry

/-! 2.3 (1 point). Explain why `error` is not a commutative monad. -/

-- enter your answer here

/-! 2.4 (1 bonus point). Prove the following composition law for `mmap`, which
holds for commutative monads.

Hint: You will need structural induction. -/

lemma mmap_mmap {m : Type → Type} [comm_lawful_monad m]
    {α β γ : Type} (f : α → m β) (g : β → m γ) (as : list α) :
  (mmap f as >>= mmap g) = mmap (λa, f a >>= g) as :=
sorry

end LoVe


section propositional

variables P Q R : Prop

------------------------------------------------
-- Proposições de dupla negaço:
------------------------------------------------

theorem doubleneg_intro :
  P → ¬¬P  :=
begin
  intro h,
  intro g,
  contradiction,
end

theorem doubleneg_elim :
  ¬¬P → P  :=
begin
  by_cases h: P,
  {intro g,
  exact h,
  },
  {intro g,
  have q:= g h,
  contradiction,}
end

theorem doubleneg_law :
  ¬¬P ↔ P  :=
begin
  split,
  have k := doubleneg_elim P,
  exact k,
  have k := doubleneg_intro P,
  exact k,
end

------------------------------------------------
-- Comutatividade dos ∨,∧:
------------------------------------------------

theorem disj_comm :
  (P ∨ Q) → (Q ∨ P)  :=
begin
  intro h,
  cases h with hp hq,
  right,
  assumption,
  left,
  assumption,
end

theorem conj_comm :
  (P ∧ Q) → (Q ∧ P)  :=
begin
  intro h,
  cases h with hp hq,
  split,
  exact hq,
  exact hp,
end

------------------------------------------------
-- Proposições de interdefinabilidade dos →,∨:
------------------------------------------------

theorem impl_as_disj_converse :
  (¬P ∨ Q) → (P → Q)  :=
begin
  intro h,
  intro p,
  cases h with hnp hq,
  contradiction,
  exact hq,
end

theorem disj_as_impl :
  (P ∨ Q) → (¬P → Q)  :=
begin
  intro h,
  intro np,
  cases h with p q,
  contradiction,
  exact q,
end

------------------------------------------------
-- Proposições de contraposição:
------------------------------------------------

theorem impl_as_contrapositive :
  (P → Q) → (¬Q → ¬P)  :=
begin
  intro h,
  intro nq,
  intro p,
  have q := h p,
  contradiction,
end

theorem impl_as_contrapositive_converse :
  (¬Q → ¬P) → (P → Q)  :=
begin
  intro h,
  intro p,
  by_cases lemQ: Q,
  exact lemQ,
  have np := h lemQ,
  contradiction,
end

theorem contrapositive_law :
  (P → Q) ↔ (¬Q → ¬P)  :=
begin
  split,
  have k := impl_as_contrapositive P Q,
  exact k,
  have k := impl_as_contrapositive_converse P Q,
  exact k,
end

------------------------------------------------
-- A irrefutabilidade do LEM:
------------------------------------------------

theorem lem_irrefutable :
  ¬¬(P∨¬P)  :=
begin
  intro nLEM,
  have LEM : P ∨ ¬P,
    right,
    intro p,
    have re_LEM : P ∨ ¬P,
      left,
      exact p,
      contradiction,
  contradiction,
end

------------------------------------------------
-- A lei de Peirce
------------------------------------------------

theorem peirce_law_weak :
  ((P → Q) → P) → ¬¬P  :=
begin
  intro h,
  intro np,
  have h_contraposer := impl_as_contrapositive (P → Q) P,
  have contrapos_h := h_contraposer h,
  have n_ptoq := contrapos_h np,
  have ptoq : P → Q,
    intro p,
    contradiction,
  contradiction,
end

-------------------
-- Estou ciente de que o jeito acima é mais trabalhoso do que o ideal. Fiz-lhe assim e assim manter-lhe-ei por fins científicos somente.
-------------------

------------------------------------------------
-- Proposições de interdefinabilidade dos ∨,∧:
------------------------------------------------

theorem disj_as_negconj :
  P∨Q → ¬(¬P∧¬Q)  :=
begin
  intro porq,
  intro npnq,
  cases npnq with np nq,
  cases porq with p q,
  contradiction,
  contradiction,
end

theorem conj_as_negdisj :
  P∧Q → ¬(¬P∨¬Q)  :=
begin
  intro pq,
  intro npRnq,
  cases pq with p q,
  cases npRnq with np nq,
  contradiction,
  contradiction,
end

------------------------------------------------
-- As leis de De Morgan para ∨,∧:
------------------------------------------------

theorem demorgan_disj :
  ¬(P∨Q) → (¬P ∧ ¬Q)  :=
begin
  intro npRq,
  split,
  intro p,
  have pRq : P ∨ Q,
    left,
    exact p,
  contradiction,
  intro q,
  have pRq : P ∨ Q,
    right,
    exact q,
  contradiction,
end

theorem demorgan_disj_converse :
  (¬P ∧ ¬Q) → ¬(P∨Q)  :=
begin
  intro npnq,
  intro pRq,
  cases npnq with np nq,
  cases pRq with p q,
    contradiction,
    contradiction,
end

theorem demorgan_conj :
  ¬(P∧Q) → (¬Q ∨ ¬P)  :=
begin
  intro npq,
  by_cases lemp : P,
  left,
  by_contradiction nnq,
  have cont :P∧Q,
    split,
    exact lemp,
    exact nnq,
  contradiction,
  right,
  exact lemp,
end

theorem demorgan_conj_converse :
  (¬Q ∨ ¬P) → ¬(P∧Q)  :=
begin
  intro nqRnp,
  intro pq,
  cases pq with p q,
  cases nqRnp with nq np,
    contradiction,
    contradiction,
end

theorem demorgan_conj_law :
  ¬(P∧Q) ↔ (¬Q ∨ ¬P)  :=
begin
  split,
  have k := demorgan_conj P Q,
  exact k,
  have k := demorgan_conj_converse P Q,
  exact k,
end

theorem demorgan_disj_law :
  ¬(P∨Q) ↔ (¬P ∧ ¬Q)  :=
begin
  split,
  have k := demorgan_disj P Q,
  exact k,
  have k := demorgan_disj_converse P Q,
  exact k,
end

------------------------------------------------
-- Proposições de distributividade dos ∨,∧:
------------------------------------------------

theorem distr_conj_disj :
  P∧(Q∨R) → (P∧Q)∨(P∧R)  :=
begin
  intro pqRr,
  cases pqRr with p qRr,
  cases qRr with q r,
  left,
  have pq : P ∧ Q,
    split,
    exact p,
    exact q,
  exact pq,
  right,
  have pr : P ∧ R,
    split,
    exact p,
    exact r,
  exact pr,
end

theorem distr_conj_disj_converse :
  (P∧Q)∨(P∧R) → P∧(Q∨R)  :=
begin
  intro pqRpr,
  cases pqRpr with pq pr,
    cases pq with p q,
    split,
    exact p,
    left,
    exact q,
    cases pr with p r,
    split,
    exact p,
    right,
    exact r,

end

theorem distr_disj_conj :
  P∨(Q∧R) → (P∨Q)∧(P∨R)  :=
begin
  intro pRqr,
  cases pRqr with p qr,
  split,
  left,
  exact p,
  left,
  exact p,
  cases qr with q r,
  split,
  right,
  exact q,
  right,
  exact r,
end

theorem distr_disj_conj_converse :
  (P∨Q)∧(P∨R) → P∨(Q∧R)  :=
begin
  intro pRqpRr,
  cases pRqpRr with pRq pRr,
  cases pRq with p q,
  left,
  exact p,
  cases pRr with p r,
  left,
  exact p,
  right,
  have qr : Q∧R,
    split,
    exact q,
    exact r,
  exact qr,
end

------------------------------------------------
-- Currificação
------------------------------------------------

theorem curry_prop :
  ((P∧Q)→R) → (P→(Q→R))  :=
begin
  intro pqTr,
  intro p,
  intro q,
  have pq : P∧Q,
    split,
    exact p,
    exact q,
  have r := pqTr pq,
  exact r,
end

theorem uncurry_prop :
  (P→(Q→R)) → ((P∧Q)→R)  :=
begin
  intro pTqTr,
  intro pq,
  cases pq with p q,
  have qTr := pTqTr p,
  have r := qTr q,
  exact r,
end

------------------------------------------------
-- Reflexividade da →:
------------------------------------------------

theorem impl_refl :
  P → P  :=
begin
  intro p,
  exact p,
end

------------------------------------------------
-- Weakening and contraction:
------------------------------------------------

theorem weaken_disj_right :
  P → (P∨Q)  :=
begin
  intro p,
  left,
  exact p,
end

theorem weaken_disj_left :
  Q → (P∨Q)  :=
begin
  intro q,
  right,
  exact q,
end

theorem weaken_conj_right :
  (P∧Q) → P  :=
begin
  intro pq,
  cases pq with p q,
  exact p,
end

theorem weaken_conj_left :
  (P∧Q) → Q  :=
begin
  intro pq,
  cases pq with p q,
  exact q,
end

theorem conj_idempot :
  (P∧P) ↔ P :=
begin
  split,
  intro pp,
  cases pp with p1 p2,
  exact p1,
  intro p,
  split,
  exact p,
  exact p,
end

theorem disj_idempot :
  (P∨P) ↔ P  :=
begin
  split,
  intro pRp,
  cases pRp with p1 p2,
  exact p1,
  exact p2,
  intro p,
  left,
  exact p,
end

end propositional

----------------------------------------------------------------

section predicate

variable U : Type
variables P Q : U -> Prop

------------------------------------------------
-- As leis de De Morgan para ∃,∀:
------------------------------------------------

theorem demorgan_exists :
  ¬(∃x, P x) → (∀x, ¬P x)  :=
begin
  intro nEpx,
  intro x,
  intro px,
  have cont : (∃x, P x),
    existsi x,
    exact px,
  contradiction,

end

theorem demorgan_exists_converse :
  (∀x, ¬P x) → ¬(∃x, P x)  :=
begin
  intro xp,
  intro nxp,
  cases nxp with x,
  have cont := xp x,
  contradiction,
end

theorem demorgan_forall :
  ¬(∀x, P x) → (∃x, ¬P x)  :=
begin
  apply impl_as_contrapositive_converse  (¬(∀x, P x)) (∃x, ¬P x),
  intro nEnpx,
  apply doubleneg_intro (∀x, P x),
  intro x,
  by_contra,
  have Enpx : ∃x, ¬P x,
    existsi x,
    exact h,
  contradiction,
end

theorem demorgan_forall_converse :
  (∃x, ¬P x) → ¬(∀x, P x)  :=
begin
  intro Exnpx,
  intro Fxpx,
  cases Exnpx with x npx,
  have px := Fxpx x,
  contradiction,
end

theorem demorgan_forall_law :
  ¬(∀x, P x) ↔ (∃x, ¬P x)  :=
begin
  split,
  have k := demorgan_forall U P,
  exact k,
  have k := demorgan_forall_converse U P,
  exact k,
end

theorem demorgan_exists_law :
  ¬(∃x, P x) ↔ (∀x, ¬P x)  :=
begin
  split,
  have k := demorgan_exists U P,
  exact k,
  have k := demorgan_exists_converse U P,
  exact k,
end

------------------------------------------------
-- Proposições de interdefinabilidade dos ∃,∀:
------------------------------------------------

theorem exists_as_neg_forall :
  (∃x, P x) → ¬(∀x, ¬P x)  :=
begin
  intro Epx,
  intro nFnpx,
  cases Epx with x px,
  have npx := nFnpx x,
  contradiction,
end

theorem forall_as_neg_exists :
  (∀x, P x) → ¬(∃x, ¬P x)  :=
begin
  intro Fxpx,
  intro Exnpx,
  cases Exnpx with x npx,
  have px := Fxpx x,
  contradiction,
end

theorem forall_as_neg_exists_converse :
  ¬(∃x, ¬P x) → (∀x, P x)  :=
begin
  intro nEnpx,
  intro x,
  by_contra npx,
  have Enpx : ∃x, ¬P x,
    existsi x,
    exact npx,
  contradiction,
end

theorem exists_as_neg_forall_converse :
  ¬(∀x, ¬P x) → (∃x, P x)  :=
begin
  intro nFnpx,
  by_contra nEpx,
  have nFnpx : ∀x, ¬P x,
    intro x,
    intro px,
    have Epx: ∃x, P x,
      existsi x,
      exact px,
    contradiction,
  contradiction,
end

theorem forall_as_neg_exists_law :
  (∀x, P x) ↔ ¬(∃x, ¬P x)  :=
begin
  split,
  have k := forall_as_neg_exists U P,
  exact k,
  have k := forall_as_neg_exists_converse U P,
  exact k,
end

theorem exists_as_neg_forall_law :
  (∃x, P x) ↔ ¬(∀x, ¬P x)  :=
begin
  split,
  have k := exists_as_neg_forall U P,
  exact k,
  have k := exists_as_neg_forall_converse U P,
  exact k,
end

------------------------------------------------
--  Proposições de distributividade de quantificadores:
------------------------------------------------

theorem exists_conj_as_conj_exists :
  (∃x, P x ∧ Q x) → (∃x, P x) ∧ (∃x, Q x)  :=
begin
  intro Expxqx,
  cases Expxqx with x pxqx,
  cases pxqx with px qx,
  split,
  existsi x,
  exact px,
  existsi x,
  exact qx,
end

theorem exists_disj_as_disj_exists :
  (∃x, P x ∨ Q x) → (∃x, P x) ∨ (∃x, Q x)  :=
begin
  intro ExpxRqx,
  cases ExpxRqx with x pxRqx,
  cases pxRqx with px qx,
  left,
  existsi x,
  exact px,
  right,
  existsi x,
  exact qx,
end

theorem exists_disj_as_disj_exists_converse :
  (∃x, P x) ∨ (∃x, Q x) → (∃x, P x ∨ Q x)  :=
begin
  intro ExpxRExqx,
  cases ExpxRExqx with Expx Exqx,
  cases Expx with x px,
  existsi x,
  left,
  exact px,
  cases Exqx with x qx,
  existsi x,
  right,
  exact qx,
end

theorem forall_conj_as_conj_forall :
  (∀x, P x ∧ Q x) → (∀x, P x) ∧ (∀x, Q x)  :=
begin
  intro Fpxqx,
  split,
  intro x,
  have pxqx := Fpxqx x,
  cases pxqx with px qx,
  exact px,
  intro x,
  have pxqx := Fpxqx x,
  cases pxqx with px qx,
  exact qx,

end

theorem forall_conj_as_conj_forall_converse :
  (∀x, P x) ∧ (∀x, Q x) → (∀x, P x ∧ Q x)  :=
begin
  intro FxpxFxqx,
  intro x,
  cases FxpxFxqx with Fxpx Fxqx,
  split,
  have px := Fxpx x,
  exact px,
  have qx := Fxqx x,
  exact qx,
end

theorem forall_disj_as_disj_forall_converse :
  (∀x, P x) ∨ (∀x, Q x) → (∀x, P x ∨ Q x)  :=
begin
  intro FxpxRFxqx,
  intro x,
  cases FxpxRFxqx with Fxpx Fxqx,
  have px := Fxpx x,
  left,
  exact px,
  have qx := Fxqx x,
  right,
  exact qx,
end

/- NOT THEOREMS --------------------------------

theorem forall_disj_as_disj_forall :
  (∀x, P x ∨ Q x) → (∀x, P x) ∨ (∀x, Q x)  :=
begin
end

theorem exists_conj_as_conj_exists_converse :
  (∃x, P x) ∧ (∃x, Q x) → (∃x, P x ∧ Q x)  :=
begin
end

---------------------------------------------- -/

end predicate


import algebra.ring
import data.real.basic
import tactic

section
variables (R : Type*) [ring R]

#check (add_assoc : ∀ a b c : R, a + b + c = a + (b + c))
#check (add_comm : ∀ a b : R, a + b = b + a)
#check (zero_add : ∀ a : R, 0 + a = a)
#check (add_left_neg : ∀ a : R, -a + a = 0)
#check (mul_assoc : ∀ a b c : R, a * b * c = a * (b * c))
#check (mul_one : ∀ a : R, a * 1 = a)
#check (one_mul : ∀ a : R, 1 * a = a)
#check (mul_add : ∀ a b c : R, a * (b + c) = a * b + a * c)
#check (add_mul : ∀ a b c : R, (a + b) * c = a * c + b * c)
end

section
variables (R : Type*) [comm_ring R]
variables a b c d : R

example : (c * b) * a = b * (a * c) :=
by ring

example : (a + b) * (a + b) = a * a + 2 * (a * b) + b * b :=
by ring

example : (a + b) * (a - b) = a^2 - b^2 :=
by ring

example (hyp : c = d * a + b) (hyp' : b = a * d) :
  c = 2 * a * d :=
begin
  rw [hyp, hyp'],
  ring
end
end

namespace my_ring
variables {R : Type*} [ring R]

theorem add_zero (a : R) : a + 0 = a :=
by rw [add_comm, zero_add]

theorem add_right_neg (a : R) : a + -a = 0 :=
by rw [add_comm, add_left_neg]

#check @my_ring.add_zero
#check @add_zero

end my_ring

namespace my_ring
variables {R : Type*} [ring R]

theorem neg_add_cancel_left (a b : R) : -a + (a + b) = b :=
by rw [←add_assoc, add_left_neg, zero_add]

/- Prove these: -/

theorem add_neg_cancel_right (a b : R) : (a + b) + -b = a :=
sorry

theorem add_left_cancel {a b c : R} (h : a + b = a + c) : b = c :=
sorry

theorem add_right_cancel {a b c : R} (h : a + b = c + b) : a = c :=
sorry

theorem mul_zero (a : R) : a * 0 = 0 :=
begin
  have h : a * 0 + a * 0 = a * 0 + 0,
  { rw [←mul_add, add_zero, add_zero] },
  rw add_left_cancel h
end

theorem zero_mul (a : R) : 0 * a = 0 :=
sorry

theorem neg_eq_of_add_eq_zero {a b : R} (h : a + b = 0) : -a = b :=
sorry

theorem eq_neg_of_add_eq_zero {a b : R} (h : a + b = 0) : a = -b :=
sorry

theorem neg_zero : (-0 : R) = 0 :=
begin
  apply neg_eq_of_add_eq_zero,
  rw add_zero
end

theorem neg_neg (a : R) : -(-a) = a :=
sorry

end my_ring

/- Examples. -/

section
variables {R : Type*} [ring R]

example (a b : R) : a - b = a + -b :=
sub_eq_add_neg a b

end

example (a b : ℝ) : a - b = a + -b :=
rfl

example (a b : ℝ) : a - b = a + -b :=
by reflexivity

namespace my_ring

variables {R : Type*} [ring R]

theorem self_sub (a : R) : a - a = 0 :=
begin
  rw [sub_eq_add_neg, add_right_neg],
end

-- Next one works by the definition of subtraction in `ℝ`
theorem self_sub' (a : ℝ) : a - a = 0 := add_right_neg a

lemma one_add_one_eq_two : 1 + 1 = (2 : R) :=
by refl

theorem two_mul (a : R) : 2 * a = a + a :=
sorry

end my_ring

section
variables (A : Type*) [add_group A]

#check (add_assoc : ∀ a b c : A, a + b + c = a + (b + c))
#check (zero_add : ∀ a : A, 0 + a = a)
#check (add_left_neg : ∀ a : A, -a + a = 0)
end

section
variables {G : Type*} [group G]

#check (mul_assoc : ∀ a b c : G, a * b * c = a * (b * c))
#check (one_mul : ∀ a : G, 1 * a = a)
#check (mul_left_inv : ∀ a : G, a⁻¹ * a = 1)

namespace my_group

theorem mul_right_inv (a : G) : a * a⁻¹ = 1 :=
sorry

theorem mul_one (a : G) : a * 1 = a :=
sorry

theorem mul_inv_rev (a b : G) : (a * b)⁻¹ = b⁻¹ * a ⁻¹ :=
sorry

end my_group
end


import data.set -- handy for proofs.

definition A : set ℕ := {1,2,3,4,5}

-- part (a) is true

theorem M1F_Sheet01_Q05a_is_true : 1 ∈ A :=
begin
simp [A]
end

-- part (b) does not even typecheck.

-- part (c) is true

theorem M1F_Sheet01_Q05c_is_true : {1} ⊆ A :=
begin
simp [A]
end

-- part (d) is true

-- set_option pp.notation false
theorem M1F_Sheet01_Q05d_is_true : {1,2} ⊆ A :=
begin
simp [A], -- eew doesn't work!
simp [has_subset.subset,set.subset], -- found using set_option pp.notation false
-- goal is now
-- ∀ ⦃a : ℕ⦄, a = 1 ∨ a = 2 → a = 1 ∨ a = 2 ∨ a = 3 ∨ a = 4 ∨ a = 5
intro a,
intro H,
cases H,
  left,assumption,
  right,left,assumption
  -- more painful than I was expecting.
end

-- part (e) is true. Proof basically same as (d).

theorem M1F_Sheet01_Q05e_is_true : {1,2,1} ⊆ A :=
begin
simp [A],
simp [has_subset.subset,set.subset],
-- goal is again
-- ∀ ⦃a : ℕ⦄, a = 1 ∨ a = 2 → a = 1 ∨ a = 2 ∨ a = 3 ∨ a = 4 ∨ a = 5
intro a,
intro H,
cases H,
  left,assumption,
  right,left,assumption
end

-- part (f) doesn't typecheck

-- part (g) doesn't either

-- part (h) is true

set_option pp.notation false

theorem M1F_Sheet01_Q05h_is_true : A ⊇ A :=
begin
unfold superset,
simp [A],
simp [has_subset.subset,set.subset]
-- wooah that worked!
-- Can use set_option trace.simplify.rewrite true to see why :-)
end

-- Notation (to remind Kevin)
-- \ sub for subset,
-- \ sup for superset
-- \ empty for empty set
-- \ un for union
-- \ i for intersection
-- \ \ for set-theoretic difference.


import tactic

variable A : Type

-- Exercise 4(a): Show that meet and join are unique (if they exist)
def isUpperBound {A} [partial_order A] (M : set A) (a : A) : Prop :=
  ∀ m ∈ M, m ≤ a

def isJoin {A} [partial_order A] (M : set A) (a : A) : Prop :=
  isUpperBound M a ∧ ∀ a' : A, (∀ m ∈ M, m ≤ a') → a ≤ a'

lemma join_is_unique [partial_order A] {M : set A} {a b : A} (aM : isJoin M a) (bM : isJoin M b) : a = b :=
begin
  apply le_antisymm,
  {
    show a ≤ b,
    apply aM.right,
    exact bM.left,
  },
  {
    show b ≤ a,
    apply bM.right,
    exact aM.left,
  },
end

def isLowerBound {A} [partial_order A] (M : set A) (a : A) : Prop :=
  ∀ m ∈ M, m ≥ a

def isMeet {A} [partial_order A] (M : set A) (a : A) : Prop :=
  isLowerBound M a ∧ ∀ a' : A, (∀ m ∈ M, m ≥ a') → a ≥ a'

lemma meet_is_unique [partial_order A] {M : set A} {a b : A} (aM : isMeet M a) (bM : isMeet M b) : a = b :=
begin
  apply le_antisymm,
  {
    apply bM.right,
    exact aM.left,
  },
  {
    apply aM.right,
    exact bM.left,
  },
end

-- Exercise 4(b)
-- Show that for a poset to be a complete lattice it suffices to show that every subset has a join (or every subset has a meet)
def hasAllJoins (A : Type) [partial_order A] : Prop :=
  ∀ M : set A, ∃ a : A, isJoin M a

def hasAllMeets (A : Type) [partial_order A] : Prop :=
  ∀ M : set A, ∃ a : A, isMeet M a

lemma allJoins_iff_allMeets {A} [partial_order A] : (hasAllJoins A) ↔ (hasAllMeets A) :=
begin
  split,
  {
    show hasAllJoins A → hasAllMeets A,
    intro hAllJoins,
    intro M,
    let N := {a : A | ∀ m ∈ M, m ≥ a},
    cases hAllJoins N with joinN hJoinN,
    use joinN,
    split,
    {
      show ∀ m ∈ M, m ≥ joinN,
      intro m,
      intro hmM,
      have h : ∀ n ∈ N, m ≥ n :=
      begin
        intro n,
        intro hnN,
        simp at hnN,
        exact hnN m hmM,
      end,
      exact hJoinN.right m h,
    },
    {
      show ∀ a' : A, (∀ m ∈ M, m ≥ a') → joinN ≥ a',
      intro a',
      intro h,
      exact hJoinN.left a' h,
    },
  },
  {
    -- Note that this direction is exactly the same as (dual of) the first
    -- just with all ≥ replaced by ≤ and all occurences of "join" by "meet"
    show hasAllMeets A → hasAllJoins A,
    intro hAllMeets,
    intro M,
    let N := {a : A | ∀ m ∈ M, m ≤ a},
    cases hAllMeets N with meetN hMeetN,
    use meetN,
    split,
    {
      show ∀ m ∈ M, m ≤ meetN,
      intro m,
      intro hmM,
      have h : ∀ n ∈ N, m ≤ n :=
      begin
        intro n,
        intro hnN,
        simp at hnN,
        exact hnN m hmM,
      end,
      exact hMeetN.right m h,
    },
    {
      show ∀ a' : A, (∀ m ∈ M, m ≤ a') → meetN ≤ a',
      intro a',
      intro h,
      exact hMeetN.left a' h,
    },
  },
end

-- Exercise 4(c)
--


import ..lectures.love11_logical_foundations_of_mathematics_demo

/-! # LoVe Exercise 11: Logical Foundations of Mathematics -/

set_option pp.beta true
set_option pp.generalized_field_notation false

namespace LoVe

/-! ## Question 1: Vectors as Subtypes

Recall the definition of vectors from the demo: -/

#check vector

/-! The following function adds two lists of integers elementwise. If one
function is longer than the other, the tail of the longer function is
truncated. -/

def list.add : list ℤ → list ℤ → list ℤ
| []        []        := []
| (x :: xs) (y :: ys) := (x + y) :: list.add xs ys
| []        (y :: ys) := []
| (x :: xs) []        := []

/-! 1.1. Show that if the lists have the same length, the resulting list also
has that length. -/

lemma list.length_add :
  ∀(xs : list ℤ) (ys : list ℤ) (h : list.length xs = list.length ys),
    list.length (list.add xs ys) = list.length xs
| []        []        :=
  sorry
| (x :: xs) (y :: ys) :=
  sorry
| []        (y :: ys) :=
  sorry
| (x :: xs) []        :=
  sorry

/-! 1.2. Define componentwise addition on vectors using `list.add` and
`list.length_add`. -/

def vector.add {n : ℕ} : vector ℤ n → vector ℤ n → vector ℤ n
:= sorry

/-! 1.3. Show that `list.add` and `vector.add` are commutative. -/

lemma list.add.comm :
  ∀(xs : list ℤ) (ys : list ℤ), list.add xs ys = list.add ys xs
:= sorry

lemma vector.add.comm {n : ℕ} (x y : vector ℤ n) :
  vector.add x y = vector.add y x :=
sorry

/-! ## Question 2: Integers as Quotients

Recall the construction of integers from the lecture, not to be confused with
Lean's predefined type `int` (= `ℤ`): -/

#check int.rel
#check int.rel_iff
#check int

/-! 2.1. Define negation on these integers. -/

def int.neg : int → int :=
sorry

/-! 2.2. Prove the following lemmas about negation. -/

lemma int.neg_eq (p n : ℕ) :
  int.neg ⟦(p, n)⟧ = ⟦(n, p)⟧ :=
sorry

lemma int.neg_neg (a : int) :
  int.neg (int.neg a) = a :=
sorry

end LoVe


/-
Copyright (c) 2022 Kevin Buzzard. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author : Kevin Buzzard
-/

import tactic -- imports all the Lean tactics
import data.real.basic -- imports the real numbers

/-

# Limits of sequences in Lean

We give the standard `ε`, `N` definition of the limit of a sequence
and prove some theorems about them.

## lambda (λ) notation for functions

Here's how we define the functions from the naturals to the naturals
sending n to n^2 + 3:

-/

def f : ℕ → ℝ := λ n, n^2 + 3

/-

Mathematicians might write `n ↦ n^2+3` for this functions; indeed `λ` is
just prefix notation for the infix notation `↦` (i.e. you write `λ` at
the front and `↦` in the middle but they mean the same thing). You can
read more about function types in the "three kinds of types" section
of Part B of the course book.

Sometimes you might find yourself with a lambda-defined function
evaluated at a number. For example, you might see something like
`(λ n, n^2 + 3) 37`, which means "take the function sending
`n` to `n^2+3` and then evaluate it at 37". You can use the `dsimp`
(or `dsimp only`) tactic to simplify this to `37^2+3`.

The reason we need to know about function notation for this sheet
is that a sequence `x₀, x₁, x₂, …` of reals on this sheet will
be encoded as a function from `ℕ` to `ℝ` sending `0` to `x₀`, `1` to `x₁`
and so on.

## Limit of a sequence.

Here's the definition of the limit of a sequence.
-/

/-- If `a(n)` is a sequence of reals and `t` is a real, `tends_to a t`
is the assertion that the limit of `a(n)` as `n → ∞` is `t`. -/
def tends_to (a : ℕ → ℝ) (t : ℝ) : Prop :=
∀ ε > 0, ∃ B : ℕ, ∀ n, B ≤ n → |a n - t| < ε

/-

We've made a definition, so it's our job to now make the API
for the definition, i.e. prove some basic theorems about it.
-/

-- If your goal is `tends_to a t` and you want to replace it with
-- `∀ ε > 0, ∃ B, …` then you can do this with `rw tends_to_def`.
theorem tends_to_def {a : ℕ → ℝ} {t : ℝ} :
  tends_to a t ↔ ∀ ε, 0 < ε → ∃ B : ℕ, ∀ n, B ≤ n → |a n - t| < ε :=
begin
  -- true by definition
  refl,
end

-- the eagle-eyed viewers amongst you might have spotted
-- that `∀ ε > 0, ...` and `∀ ε, ε > 0 → ...` and `∀ ε, 0 < ε → ...`
-- are all definitionally equal, so `refl` sees through them.

/-

## The questions

Here are some basic results about limits of sequences.
See if you can fill in the `sorry`s with Lean proofs.
Note that `norm_num` can work with `|x|` if `x` is a numeral like 37,
but it can't do anything with it if it's a variable.
-/

/-- The limit of the constant sequence with value 37 is 37. -/
theorem tends_to_thirtyseven : tends_to (λ n, 37) 37 :=
begin
  rw tends_to_def,
  intros ε hε,
  use 100,
  intros n hn,
  norm_num,
  exact hε,
end

/-- The limit of the constant sequence with value `c` is `c`. -/
theorem tends_to_const (c : ℝ) : tends_to (λ n, c) c :=
begin
  intros ε hε,
  dsimp only,
  use 37,
  intros n hn,
  ring_nf,
  norm_num,
  exact hε,
end

/-- If `a(n)` tends to `t` then `a(n) + c` tends to `t + c` -/
theorem tends_to_add_const {a : ℕ → ℝ} {t : ℝ} (c : ℝ)
  (h : tends_to a t) :
  tends_to (λ n, a n + c) (t + c) :=
begin
  rw tends_to_def at h ⊢,
  ring_nf,
  exact h,
end

/-- If `a(n)` tends to `t` then `-a(n)` tends to `-t`.  -/
example {a : ℕ → ℝ} {t : ℝ} (ha : tends_to a t) :
  tends_to (λ n, - a n) (-t) :=
begin
  rw tends_to_def at ha ⊢,
  intros ε hε,
  specialize ha ε hε,
  cases ha with B hB,
  use B,
  intros n hn,
  specialize hB n hn,
  have h : ∀ (x : ℝ), |(-x)| = |x|,
    sorry, -- how to prove basic results about |.|? See next sheet!
  rw ← h,
  ring_nf at hB ⊢,
  exact hB,
end


import .love11_logical_foundations_of_mathematics_demo

/- # LoVe Exercise 11: Logical Foundations of Mathematics -/

set_option pp.beta true
set_option pp.generalized_field_notation false

namespace LoVe

/- ## Question 1: Vectors as Subtypes

Recall the definition of vectors from the demo: -/

#check vector

/- The following function adds two lists of integers elementwise. If one
function is longer than the other, the tail of the longer function is
truncated. -/

def list.add : list ℤ → list ℤ → list ℤ
| []        []        := []
| (x :: xs) (y :: ys) := (x + y) :: list.add xs ys
| []        (y :: ys) := []
| (x :: xs) []        := []

/- 1.1. Show that if the lists have the same length, the resulting list also
has that length. -/

lemma list.length_add :
  ∀(xs : list ℤ) (ys : list ℤ) (h : list.length xs = list.length ys),
    list.length (list.add xs ys) = list.length xs
| []        []        :=
  by simp [list.add]
| (x :: xs) (y :: ys) :=
  begin
    simp [list.add, length],
    intro h,
    rw list.length_add xs ys h
  end
| []        (y :: ys) :=
  begin
    intro h,
    cases' h
  end
| (x :: xs) []        :=
  begin
    intro h,
    cases' h
  end

/- 1.2. Define componentwise addition on vectors using `list.add` and
`list.length_add`. -/

def vector.add {n : ℕ} : vector ℤ n → vector ℤ n → vector ℤ n
| u v := subtype.mk (list.add (subtype.val u) (subtype.val v))
  begin
    rw list.length_add,
    { exact subtype.property u },
    { rw subtype.property u, rw subtype.property v }
  end

/- 1.3. Show that `list.add` and `vector.add` are commutative. -/

lemma list.add.comm :
  ∀(xs : list ℤ) (ys : list ℤ), list.add xs ys = list.add ys xs
| []        []        := by refl
| (x :: xs) (y :: ys) := by simp [list.add, add_comm]; exact list.add.comm xs ys
| []        (y :: ys) := by refl
| (x :: xs) []        := by refl

lemma vector.add.comm {n : ℕ} (x y : vector ℤ n) :
  vector.add x y = vector.add y x :=
begin
  apply subtype.eq,
  simp [vector.add],
  apply list.add.comm
end

/- ## Question 2: Integers as Quotients

Recall the construction of integers from the lecture, not to be confused with
Lean's predefined type `int` (= `ℤ`): -/

#check int.rel
#check int.rel_iff
#check int

/- 2.1. Define negation on these integers. -/

def int.neg : int → int :=
quotient.lift (λpn : ℕ × ℕ, ⟦(prod.snd pn, prod.fst pn)⟧)
  begin
    intros a a' ha,
    cases' a with p n,
    cases' a' with p' n',
    apply quotient.sound,
    simp at *,
    rw int.rel_iff at *,
    cc
  end

/- 2.2. Prove the following lemmas about negation. -/

lemma int.neg_eq (p n : ℕ) :
  int.neg ⟦(p, n)⟧ = ⟦(n, p)⟧ :=
by refl

lemma int.neg_neg (a : int) :
  int.neg (int.neg a) = a :=
begin
  apply quotient.induction_on a,
  intro a,
  cases' a,
  simp [int.neg_eq]
end

end LoVe