/-
*** MATHEMATICAL LOGIC ***

/- *** Overview *** -/

In this unit you will learned the following concepts, among others:

* mathematical logics as formal systems
* propositions, including their syntax and semantics
* truth judgments
* inference rules, their meaning and notation
* axioms
* proofs as evidence that justify truth judgments
* propositions about equality
* automating propositions and proofs in Lean
* an axiom defining equality in general
* type judgments
* proof trees (derivations)
* set theory and type theory as axiomatic foundations for mathematics
-/

/- *** Formal systems *** -/

/-
Modern mathematics, and discrete mathematics in particular,
are formal (mathematical) logical systems.

Logical systems in turn are rooted in the concepts of
propositions, truth judgments, inference rules, and
proofs, or derivations, as evidence supporting truth
judgments.

This is pretty abstract. Let's see what it really means.
-/

/- ** Propositions and truth judgments ** -/

/-
A proposition is a mathematically precise assertion that
some state of affairs is true in some domain of interest.

For example, in the domain of basic arithmetic, the claim
that 0 = 0 is a proposition. So is the claim that 0 = 1.

As another example, in the domain of some family unit, a
perfectly good proposition is that "Mary is the mother of
Bob." It might or might not be true in a given family,
but it's a perfectly good proposition: a claim that a
certain state of affairs holds in that domain.

Logic, then, is about making propositions precise and
above precise rules for ascertaining when any given
proposition can be judged to be true.

Clearly we would judge the proposition, 0 = 0, to be
true, in the domain of arithmetic. Similarly, we would
not judge the proposition 0 = 1 to be true.

Logicians will sometimes write "0 = 0 : true" as a
way to assert that the proposition, 0 = 0, is (has
been judged to be) true. We call this a truth
judgment. More generally, if P is a proposition,
then "P : true" denotes the judgment that P is true.

EXERCISE: Write a truth judgment (just type it
in as part of this comment) for the proposition
that "Mary is the mother of Bob."

Another example of a proposition is the claim that
zero does not equal one, which we would write like
this: ¬ (0 = 1). You could pronounce this as "it is
NOT the case that 0 = 1." We would naturally judge
this proposition to be true (albeit currently just
based on our intution, not on any specific rules).

EXERCISE: Write a truth judgment here (just type it
in as part of this comment) that expresses the
judgement that ¬ (0 = 1) is true.

Propositions, then, are claims that certain states
of affairs hold, and logic provides us with rules
for determing when a given proposition is (can be
judged to be) true.

Propositions are basically declarative statements,
asserting that "such and such" is true. What makes
them, and logic, formal is that they have a precise
syntax, or form, and a precise semantics, or meaning.

/- * syntax * -/

Just as with computer programs, there are strict
rules that define the forms that propositions can
take, i.e., their syntax. For example, 0 = 0 is a
syntactically well-formed proposition, but 00= is
not.

/- * semantics *-/

Moreover, propositions in a given logic also have
meanings, in that they can be judged to be true,
or not, in a given domain. For example "Mary is
the mom of Bob" (perhaps written more formally as
mother_of(Mary,Bob) is a proposition that can be
judged to be true in some domains (family units)
and not true in others. It's true in a domain in
which Mary really is the mother of Bob, and it is
not true otherwise.

A proposition cannot generally be judged to be
true or false on its own. Rather, it is judged
in some domain: under an *interpretation* that
explains what each symbol in the proposition is
meant  to refer to.

For example, we could judge "Mary is the mother
of Bob" to be true if and only if "Mary" refers
to some person, "Bob" refers to another person,
and under some definition of what it means to
be the mother of, that the person referred to
as Mary really is the mother of that person
referred to as Bob.

When we talk about the semantics of a logic, we
are talking about rules for determining when some
given proposition can be judged to be true with
respect to some particular interpretation that
"maps" the symbols in the proposition to things
in the domain of discourse.

The rules for determining whether a proposition
in a particular logic is true in a given such a
domain and interpretation is, again, called the
semantics of that logic.

Logics thus provide rules that define the syntax
and the semantics of propositions: their forms,
and their meanings (that is, whether they are,
i.e., can be judged to be, true or not) under
any given interpretation.

We'll dive deeper into the syntax and semantics
of various logics as we go along. In particular,
we will discuss a simple logic, propositional
logic, and a much more useful logic, the logic
of everyday mathematics and computer science,
called predicate logic.

For purposes of this unit, we'll just assume
that one particular form of valid proposition
in predicate logic is a proposition that the
values of two terms are equal. For example,
0 = 0, 1 + 1 = 2, and 1 + 1 = 3 are valid
(syntactically well formed) propositions in
the predicate logic of everyday mathematics
and computer science.
-/

/- ** Inference rules ** -/

/-
So how do we decide whether a given proposition
can be judged to be true or not? Here is where
the semantics of a logic come into play.

The semantics of a logic comprises a set of rules,
called inference rules, that define the conditions
under which a given proposition can be judged to
be true.

Oversimplying a bit, if you can apply one or more
inference rules to propositions you already know
to be true, and if by doing this you "deduce" some
new proposition, then you can conclude that that
new proposition must also be true. Such a "chain"
of inference rules, linking things already known
to be true to propositions that you want to prove
to be true because they follow logically is called
a proof. A valid proof is incontrovertible evidence
that the final proposition is true.

An inference rule is like a little program: it says,
if you can give me evidence (i.e., proofs) showing
that certain "input" propositions can be judged to
be true, then I will hand you back evidence (i.e.,
a proof) that shows that some new proposition can
also be judged to be true. We would say that from
the  proofs of the premises (the input propositions
already known to be true), the rule derives or deduces
a proof of the conclusion.

Logicians often write inference rules like this:

  list of input truth judgments
  ----------------------------- (name-of-rule)
  truth judgment for conclusion

The required input judgments, called premises (or
antecedents), are listed above the line. The name
of the rule is given to the right of the line. And
the proposition (or consequent) that can thereby
be judged to be true (the conclusion of the rule)
is written below the line.

For example, if we already have the truth judgment,
(0 = 0 : true), and another, (1 = 1: true), then the
inference rule that logicians call "and introduction"
(or "conjunction introduction") can be used to derive
a truth judgment for the new proposition, "0 = 0 and
1 = 1", typically written as 0 = 0 ∧ 1 = 1. (Hover
your mouse over special symbols in this editor to
learn how to use them in your work.)

Predicate logic will thus (in effect) include this
rule as an inference rule:

0 = 0 : true, 1 = 1 : true
-------------------------- and-introduction-*
      0 = 0 ∧ 1 = 1

This can be pronounced as, "If you already have
evidence (a proof) supporting the judgment that
0 = 0 is true, and if you also have evidence (a
proof) supporting the judgment that 1 = 1 is true,
then by applying the and-introduction-* rule,
you can deduce (obtain a proof justifying a truth
judgment for) the proposition, 0 = 0 ∧  1 = 1".

We've put a * on the name of this rule to indicate
that it's really just a special case of a far more
general inference rule for reasoning about equality.

Inference rules are usually written not in terms of
very specific propositions, such as 0 = 0, but in
terms of variables that can refer to any arbitrary
propositions. They are often called meta-variables.

In this way, inference rules become program-like, in
that they can take arbitrary inputs (of the correct
types), and whenever they are given such inputs, they
produce result of a promised type.

Here's a simple example of such a parameterized and
thereby generalized rule. If P is *any* proposition
(e.g., it could be 0 = 0 but might be some other
proposition), and Q is another proposition (e.g.,
1 = 1), and if both propositions are already known
to be true, then you can always conclude that the
proposition "P and Q", written P ∧ Q, must also be
true, for whatever propositions P and Q happen to
be.

Here is how the general form of this inference rule
would typically be written in a book on logic.

P : true, Q : true
------------------ (and-introduction)
   P ∧ Q : true

Now here we're going to pull a little trick.
It's based on the idea that a "proof", whatever
that is (!), serves as "evidence" that justifies
a truth judgment about a proposition. So we are
now technically distinguishing between a proof
of a proposition and the truth judgment that it
justifies. Nevertheless, we will assume that a
truth judgment for some proposition, P, is in
fact justified if and only if we have a proof
of P.

To denote the fact that we have a proof,
let's call it, p, of some proposition, P,
we will write p : P. Now, the idea is that
if we have p : P then we also have P : true,
and the only way we can get P : true is if
we have some evidence, p, (a proof) for P.

In other words, P is true if and only if we
have evidence (a proof), p, for P. We can
write this statement in the language of logic
like this: "p : P ↔ P : true".  The evidence,
p, for P, justifies the judgement, P : true,
and the only way to justify such a judgement
is with such a piece of evidence. The bottom
line: having a proof of P is equivalent to P
being true.

We can thus write an equivalent inference
rule like this (compare with the previous):

p : P, q : Q
------------ (and-introduction)
 r : P ∧ Q

This then says, "If you give me a proof, p, of
some proposition, P, and if you also given me a
proof, q, of some proposition Q, then I promise
to give you back a proof, r, of the proposition,
P ∧ Q."

As a shorthand, logicians usually leave off the
": true" bits and/or the "p :" bits, and so you'd
usually see this rule written simply like this:

   P, Q
   ----- and-introduction
   P ∧ Q

Above the line, then, is a list of propositions
that must be true (i.e., for which there must
already be proofs) for the rule to be applied. Below
the line is a new proposition, the conclusion,
which typically involves some premise propositions.
An inference rule allows you to conlude that the
conclusion is true whenever you already know that
the premises are true.

A nice way to think about such a rule is as a
little program. Let's call it "and-introduction!"
It takes two arguments: namely a proof, p, of P,
and a proof, q, of Q. When these are provided as
arguments, the program constructs and returns a
proof, r, of P ∧ Q. How it might do this and what
such a proof might look like are details that we
will get to shortly.
-/

/-
EXERCISE: What is returned when the and-introduction
inference rule, viewed as a program, is applied to two
proofs: one of the proposition, 0 = 0, and one of the
proposition, "Hello" = "Hello"?

EXERCISE: Why could this rule never be applied (in
any reasonable logic) to produce a proof (thus a
truth judgement) for the proposition, 0 = 0 ∧ 0 = 1?
-/

/- *** Axioms *** -/

/-
In a mathematical or logical system, some propositions
are taken to be true unconditionally: without the need
for any prior "input" judgements or proofs.

An inference rule that requires no proof or truth
judgement inputs, and that nevertheless lets you
judge some proposition to be true unconditionally,
(e.g., by simply handing you a proof of or a truth
judgment for that proposition), is called an axiom.

We also use the term, axiom, to refer to the
proposition that is the conclusion of the rule,
and that is thereby judged true unconditionally.

An axiom viewed as an inference rule with no
proofs/judgements as inputs would be written
with no proof/judgment premises/arguments above
the line.

For example, if we were to take the proposition,
0 = 0, as an axiom, we could write it like this:

           <---- note the absence of premises
-----
0 = 0

What this says then is that without having proved
or judged any other propositions to be true you can
nevertheless assume that 0 = 0 is true. Equivalently,
without providing any proofs of other propositions
as arguments, this rule will still hand you back a
proof of 0 = 0, thereby justifying the judgment that
0 = 0 is true. In this case, you could say that the
logic includes 0 = 0 as an axiom.
-/

/- ** An inference rule for equality in general ** -/

/-
Intuitively you would suppose that the proposition,
0 = 0, should be true in any reasonable logical system.

There are two ways a logic could make this happen.
The first is that the logic could provide 0 = 0 as
an axiom, as we just discussed.

That'd be ok, but then we'd need similar axioms for
every other number. We'd also need similar axioms
for every object of every other type: person, car,
plant, atom, book, idea, etc. We end up with a pretty
unweildy (and infinite) set of axioms. Moreover, if
we were ever to define a new type of objects (e.g.,
digital pets), we'd have to extend the logic with
similar inference rules for every value of the new
type. (Fido = Fido, Spot = Spot, Kitty = Kitty, etc).

What would be much better would be to have just one
inference rule that basically allow us to conclude
that *any* object, or value, of any type whatsoever
is always equal to itself (and that nothing else
is ever equal to that object).

It'd go something like this: if T is any "type"
(such as natural number, car, person), and t is any
object or value of that type, T (e.g., 0 is a value
of type "natural number", or "nat"), then you can
unconditionally conclude that t = t is true.

/- * Type judgments * -/

We meet a new kind of judgment here: a type judgment.
If X is some type, and x is a value of that type, X,
we can denote this fact by writing x : X. We read this
as "x is of type X."

/- * The types of types * -/

The Lean tool that you're using here is based on a
foundational theory called type theory. In type
theory, every object (or value) has a single type.
Every parameter, such as an argument to a function.
has a type. Every expression has a type.

In Lean you can check the type of an expression
by using the #check command. Note that ℕ is math
shorthand for the type of natural numbers, i.e.,
non-negative integers (thus zero on up). Hover
your mouse over the #check commands to see what
are the types of 0, "Hi!", and tt in Lean.
-/

#check 0
#check "Hi!"
#check tt

/-
Here's the key idea: types are values, too! It
thus follows that a type must have a type. So
what is the type of a type?

Answer: If T is a type, then it's type is called
"Type".
-/

/-
EXERCISE: Use the #check command the see what is
the type of nat, bool, and string.
-/

/-
So, if T is some type, then we'd write the type
judgment, "T : Type". And if T is a type, and t
a value of type, T, then we'd also write t : T.
-/

/-
So now, with all that out of the way, we can write
the inference rule for equality that we really want.

T: Type, t : T
-------------- (eq)
    t = t

Those are now type judgments above the line. You can
understand this inference rule as saying this: "if you
give me a T that is a type (e.g., bool, nat, string),
and if you also give me a value, t, of type, T, (e.g.,
0 or true), then I will give you back a proof that
t = t. This single inference rule thus defines a very
sensible notion of equality for all values of all types
that exist or might ever be defined.

(Detail: This notion of equality is called Leibniz
equality.)

EXERCISE: Why exactly can this rule never be used to
derive a proof of the proposition that 0 = 1?

So now, rather than a separate axiom for 0 = 0,
another one for 1 = 1, another for true = true, and
yet another for Fido = Fido, so forth, we now have
a single inference rule, which we take as an axiom
(no prior proofs are required, but the types of T
and t do have to be right) that covers every possible
case, forever forward.

/- * Formalizing Leibniz equality * -/

Now we move the ball forward yet another yard or two.
In predicate logic, we could also write the inference
rule for eq like this: ∀ T: Type, ∀ t: T, t = t.

The upside-down A is called the universal quantifier
of predicate logic. You can pronounce it as "for all"
or "for any" or "if you give me any value of the given
type ..." Thus we have, "for any T of type, Type, ..."
and what then follows is a proposition in which T is
used. That following proposition also uses ∀. And the
proposition after that, t = t, is interpreted in a way
that assumes T and t have been given specific meanings
by the preceding quantifiers. We say that T and t are
"bound" in the expression, t = t. The scope of that
binding is limited to the current proposition.

You could thus pronounce the rule like this: "If you
give me any value, T, of type, Type (which is to say
that if T is any type), and if you give me any value,
t, of that type, T, then I promise to give you back
a value of type t = t.

Wait! We expected to get back a proof of t = t.

Yeah! In the logic of Lean, propositions are types
and proofs are values of these types. To read the
rule again, we could say, if you given me any type,
T, and any value, t, of that type, I promise to give
you back a value of type, t = t. But t = t is clearly
a proposition, so a value of this type is ... wait for
it ... a proof: a proof of the proposition, t = t."
-/

/-
Now, given this more general inference rule, we could
"apply" it to the case where T = nat (Lean's name for
the type of natural numbers), and where t = 0 (a value
of this type) to produce a proof of 0 = 0. We could
apply the same  rule to derive truth judgements for
1 = 1, 2 = 2, true = true, "Bob" = "Bob", and so on.
-/

/-
In Lean, this inference rule is built in (actually
defined in a library that is automatically loaded
when you start Lean). Moreover, there's a shorthand
in Lean for applying this rule to a value, t, of
some particular type, T, to produce a proof of
t = t. The shorthand is called "rfl". It can be
applied whenever you want to produce a proof that
two terms are equal.

Here's an example for the proposition, 0 = 0. Note:
mathematicians will often use the term "theorem" for a
proposition for which there is a proof of its truth.
-/

theorem zeqz: 0 = 0 := rfl

/-
Let's decode that. First, it says we're about
to establish a theorem by stating a proposition
and by giving a proof. The name that we give to
our proposition is zeqz (any variable name not
already in use would do). The proposition for
which we seek a truth judgment (proof) is 0 = 0.
The := separates the proposition from code that
is intended to produce such a proof. The proof
code here is simply rfl. This rfl "thing" in
turn looks at the value on the left side of the
equal sign. It finds 0. That's "t". rfl can then
infer that that the type of t is nat. That's T.
Given T = nat and t = 0, rfl returns a proof of
(a truth judgment for) the proposition, t = t,
where t = 0, which is to say, a proof of 0 = 0.
Lean then checks that this is a valid proof for
the proposition to be proved (also 0 = 0), which
it is, so so Lean accepts the proof as valid,
and assigns the proof as the value of zeqz.
From this point forward, zeqz can be used as
a proof -- justifying a truth judgment -- for
the proposition, 0 = 0.
-/

/-
EXERCISE: Try to use the same approach to prove
a "theorem" (call it zeqo) that 0 = 1. What goes
wrong? Hover over the red squiggles to see the
error messages say. They're cryptic, but we can
puzzle out what it's saying more or less. First,
the one over the rfl says rfl is expected to be
a proof that something (here denoted as m_2) is
equal to itself, but it is given as a putative
proof of 0 = 1, and that doesn't work because in
fact it's not the case that 0 = 1. In a funny way
Lean treats the proposition as a type and expects
the proof to be a value of that type. Here, rfl
generates a proof of "type" 0 = 0, but the "type"
of this proof object doesn't work for the type
of the proposition to be proved, "0 = 1", so Lean
rejects the proof. The red line over the zeqo
then explains that you've promised to provide a
proof, but you haven't, and so the thing you said
would be a theorem hasn't actually been proven to
be one!
-/

/- * A brief aside about terminology *-/

/-
Note: The word "theorem" in mathematics is generally
used to mean an "important" proposition that has been
proved. The word lemma is used to mean a somewhat less
important proposition that has been proved, often as
part of a larger proof of a more important theorem.
Mathematicians also use the word corrolary to refer
to a proposition the proof of which follows from the
proof of a more important theorem. You can read all
about the various words used to refer to things that
have been proved, or that are intended to be proved,
here: https://academia.stackexchange.com/questions/113819/is-it-acceptable-for-a-referee-to-suggest-changing-theorem-into-proposition.
For our purposes, we'll typically just use theorem.
-/

/-
For use in a few minutes, here's a similar
theorem (proposition with a proof) for the
proposition that one equals one.
-/

theorem oeqo : 1 = 1 := rfl

/-
EXERCISE: Use the same approach to prove that
"hello" = "hello".
EXERCISE: Does this approach work to prove that
2 = 1 + 1? You'd think it should, but those are
two different expressions on each side of =. Try
it to see if rfl can be used to generate a proof
that 2 = 1 + 1. Call your proof teqopo (for two
equals one plus one).
-/

/-
As you have now seen, Lean's notion of equality
does not mean exact equality of expressions. It
means instead the equality of what two expressions
mean: equality of the values to which they "reduce"
when you "evaluate" them. We can prove 2 = 1 + 1
using rfl because the "literal expression", 2,
reduces  to the value 2, and the function
application expression, 1 + 1 (wherein the plus
function is applied to the two arguments, 1 and 1)
also reduces to (evaluates to) 2. Now you've got
the same value on each side of the =, and rfl
will generate a valid proof of that proposition.
The Lean type checker then confirms that this proof
is of type 2 = 1 + 1, and Lean takes that proof to
be the value of teqopo.
-/

/-
EXERCISE: Prove as a theorem, tthof (a silly and
uninformative name to be sure), that 2 + 3 = 1 + 4.

EXERCISE: Prove as a theorem, hpleqhl, that "Hello "
++ "Logic! is equal to "Hello Logic!" (these values
are of type string in Lean and the ++ operator here
refers to the string concatenation function in Lean.)
-/

/- *** Applying Inference Rules *** -/

/-
As a cherry on top, let's return to the idea of
inference rules that require prior proof judgments
of other propositions as inputs. What inference
rules do in general is to build bigger proofs
from smaller ones!

For example, we now have a proof, zeqz, for the
proposition that 0 = 0, and we also have one, oeqo,
for the proposition, 1 = 1. You'd think we might
therefore be able to produce a new proof for the
proposition that 0 = 0 ∧ 1 = 1.
-/

/-
EXERCISE: What inference rule might we try to apply
to generate such a proof?
-/

/-
In Lean the and-introduction inference rule goes by
the name and.intro. It takes two arguments: a proof
for any proposition, P, and a proof for another
proposition, Q. It then returns a proof for the
proposition P ∧ Q. Here's the code!
-/

theorem zeqz_and_oeqo: 0 = 0 ∧ 1 = 1 :=
    and.intro zeqz oeqo -- study this carefully!

/-
Ok, let's decode that. We're proposing a theorem.
The proposition we aim to prove is 0 = 0 ∧ 1 = 1.
If we can generate a proof that Lean accepts, it
will become the value bound to zeqz_and_oeqo.
Finally, we propose to generate this proof by
applying the and introduction inference rule, which,
again, in Lean, is called and.intro.

This rule, and.intro, is really just a function!
In Lean you apply a function to its arguments by
first writing the function name and then writing
each of the arguments following that name. You do
not write the arguments to a function as a list
of values within parentheses as in Python or Java.

Voilà! We apply an inference rule "program" to
two smaller proofs to get a larger one and then
Lean checks automatically to make sure it's a
valid proof for the proposition that was asserted.
It actually does this by checking to see if the
type of the proof is consistent with the type of
the proposition.
-/

/-
EXERCISE: What happens if the proofs you pass to
and.intro aren't quite right? For example, try to
give zeqz as the value for both arguments. Does the
resulting proof "type check"?
-/

/-
Suppose we wanted to prove a similar theorem for
the proposition that 2 = 2 ∧ "Hi" = "Hi". We can
of course write two smaller theorems for 2 = 2
and "Hi" = "Hi". Each of these would in turn be
proved by the application of the rfl rule. Then
we could use the named proofs as inputs to the
and.intro "rule" and we'd be done. But there's
a shorter way to go: just write the proofs of
the individual propositions directly inline.
The and.intro rule needs a proof of 2 = 2, and
rfl will do. Similarly it needs a proof of "Hi"
= "Hi", and once again, rfl will so.
-/

theorem teqt_and_heqh: 2 = 2 ∧ "Hi" = "Hi" :=
  and.intro rfl rfl

/- ** Formal Systems ** -/
/-
A mathematical, or formal, system starts with a set
of inference rules including axioms (with the condition
that this starting point not be self-contradictory; so,
for example, you would never want 0 = 1 as an axiom).
It extends to include the typically infinite set of
all of theorems that can be possibly "proved" by any
number of applications of available inference rules
to already available truth judgments (or equivalently
to proofs, which in Lean are taken to be tantamount
to truth judgments).
-/

/-
EXERCISE: What are some other theorems that you could
prove using only the concepts we've discussed so far?
Formulate and prove several new theorems. Note that
Lean has a Boolean type, called bool, with the values
tt and ff (short for Boolean true and Boolean false).
Prove at least one theorem involving Boolean values.
-/

/- ** What mathematicians and some computer scientists do -/

/-
Working mathematicians and computer scientists don't sit
around all day mechanically generating theorems by applying
inference rules to already proved theorems to come up with
new theorems. That would be like typing randomly and hoping
to produce a new Shakespeare-quality play.

Rather, mathematicians and computer scientists, come up
with propositions that they believe, and hope, are true
(e.g., a proposition that asserts that some program gives
the right answer for any possible combination of inputs);
and they then have the task of producing proofs to show
that such propositions are true.

We call this the problem of "proving" that a proposition
is true. If there is already a truth judgment for (proof
of) the given proposition, it's easy: you just hand over
the existing proof. If there are proofs to which an
inference rule can be applied directly to produce a
proof for the given proposition, then you just apply the
inference rule to those proofs and you hand over the new
proof that results. In the typical case, you won't have
proofs of propositions that you need as inputs to the
inference rule that you'll ultimately want to generate
the desired proof, so you need to go off and produce
proofs of those intermediate propositions first. In this
way the task of producing the desired proof is broken
down into one or more smaller problems of producing the
proofs that are needed to feed the inference rule that
will be used to produce the final proof.

Mathematicians typically label the proposition that is
ultimately to be proved as a theorem (once there really
is a proof), and the smaller propositions to be proved
as lemmas. In Lean you can use theorem and lemma (and
a few other keywords, such as def) interchangeably.

Such a process is said to be "recursive". Axioms give
you starting points. You then apply inference rules to
produces proofs of larger and larger lemmas. Finally
you apply some inference rule to proofs of key lemmas
to produce a proof of the desired theorem.

The chain of "derivations" that thus results is what
logicians and mathematicians sometimes call a proof tree.
Such trees can be written using inference rule notation.

Here's the entire proof tree for 0 = 0 ∧ 1 = 1.

 rfl     rfl
-----   -----
0 = 0   1 = 1
-------------
0 = 0 ∧ 1 = 1

Read it from bottom to the top: to prove the desired
proposition, we need proofs of 0 = 0 and 1 = 1
respectively. Each of these in turn can be produced
by the rfl "axiom". These axioms requires no prior
proofs (truth judgments) as inputs, so you are done.
(We've left off the rule names to make the proof
tree easier to read. Typically they'd be included.)

So, coming up with a proof of a proposition that hasn't
yet been proved often feels like a "backwards chaining"
activity. When asked to prove a proposition, P, we often
try to find other propositions, such as Q and R, such that
if we had proofs of Q and R, we could then apply a valid
inference rule to prove P. One thus reduces the problem
of proving P to the sub-problems of proving Q and R. One
then backward-chains this way until one finally reaches
... axioms! 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!
-/

/-
EXERCISE: What "smaller" propositions might you want to
prove if your aim is ultimately to prove the proposition
that 5 = 1 + 4 ∧ "Strike" = "S" ++ "trike". Go ahead and
prove those smaller propositions. You can use whatever
names you want for these little "theorems", then write
the theorem that proves the final result using the lemmas
as inputs.
-/

/- ** Logical Foundations of Mathematics ** -/

/-
Modern mathematics is axiomatic. It's logical. It is
"founded" on mutually consistent axioms and inference
rules.

There is however more than one way to establish the
logical foundations of mathematics.

The most widely used axiomatic foundation comprises a
set of axioms that describes what it means to be a set,
and everything else then builds on the concept of sets.
The resulting theory is called "set theory." Set theory
is the most widely accepted and used logical foundation
for everyday mathematics.

The natural numbers for example can be "formalized" as
sets. Zero is represented by the empty set; one by the
set that contains only the empty set; two by the set
that contains that set; and so forth.

The specific set theory foundation for ordinary
mathematics is known as Zermelo-Frankl Set Theory
with the Axiom of Choice (often abbreviated as ZFC).
The axioms of ZFC capture our "naïve" view of sets
as collections of elements. It took much time and
great care, however, to craft a set of axioms that
are not self-contradicting. The original formulation
of set theory turned out to be inconsistent!

(Does the set of all sets that do not contain
themselves contain itself? If it does, then it
doesn't, and if it doesn't then it does: a real
inconsistency! In fact it was this problem that
led mathematicians to a much more careful notion
of what it means to be a set, as captured by the
axioms of ZFC.

As another example of an inconsistency, there's
a word in the English language to describe words
that describe themselves: autological. The antonym
of autological is heterological. "Polysyllabic" is
autological, but  "palindrome" is heterological.
Is the word "heterological" heterological? If the
word doesn't describe itself, then it is, but then
the word would describe itself. So, yeah, English
is inconsistent in this sense.)

The axioms of ZFC are somewhat technical; we will
not explore them in this class. What you might want
to remember is that if you want to prove even simple
mathematical proposition in a precise, fully formal
way using ZFC, it is a complex and messy affair.

In fact, it's so messy that most mathematicians trade
in rigorous but informal proofs. By informal
proofs we mean mathematical arguments written in a
stylized form of a natural language, such as English.
For example, a proof of 0 = 0 and 1 = 1 might read
like this: "To prove the proposition, which is a
conjunction, we need proofs of the two parts. The
first, 0 = 0, is proved by noting that equality is
a reflexive relation, and 1 = 1 is proved similarly.
Given that both conjuncts are so, then so is the
overall conjunction. So it is shown (QED in latin)."

Because machines aren't much good at figuring out what
natural language text means with mathematical precision,
it's nearly impossible today for computers to check that
purported informal proofs are correct. Proof checking
remains a demanding, mostly human, and social process.

When a mathematician claims to have produced a proof
of a theorem of potential significance, other experts
come together (often as reviewers for journals to which
mathematical purported proofs are generally submitted
for review and publication) to see if they can find
any errors in reasoning. They often do!

Such human proof checking has in some cases proved to
be nearly intractable. For example, in 2012, Shinichi
Mochizuki, a reclusive Japanse mathematician, quietly
posted to the web a 500-page "putative" proof of a
major mathematical conjecture (proposition for which
there is not yet a proof) in number theory and geometry.
If correct, it could revolutionize mathematics. Yet the
work is so complex and unusual that the mathematical
community even to this day has still been unable even
to comprehend the overall concept, not to mention
checking every last detail for subtle errors.
-/

/-
HOMEWORK: Read this article. https://www.sciencealert.com/nightmarish-500-page-math-proof-even-experts-can-t-understand-about-published-shinichi-mochizuki
-/

/-
An alternative foundation for mathematics (in particular
for what is called "constructive" mathematics) is available
in what is called "type theory." It's basically a different
(from ZFC) set of axioms and inference rules on which math
can be based. Not surprisingly, types, rather than sets, are
a fundamental building block of mathematics in type theory.
Sets can be modeled, but they are not built in.

From your high school math background, you probably
already have a reasonable intuition for sets as collections
of values. A type also defines a set of values, and each
value in that set has that type. But whereas a value can be
in many sets, in type theory a value has exactly one type.
Whenever you see a value, or an expression that reduces to
a value, in type theory, it thus makes sense to ask the
question, what is its type? Every well formed expression
and value in a type theory has exactly one type.

In Lean, we can ask the type of an expression (including
of a literal expression that directly denotes a value) by
using the check command. Hover your mouse over the #check
command. Note that ℕ is mathematical notation for "natural
number", the type of non-negative integers.
-/

-- The types of some literal expressions
#check 0
#check "Hello"
#check tt

-- defining a binding of an identifier to a value
def foo := 0

-- The type of a variable expression
#check foo

-- The types of some more complex expressions
#check 1 + foo
#check "Hello " ++ "Logic!"
#check tt && ff

/-
Without getting into complicated details, it will suffice
for now to say that proofs are much more tractable objects
in type theory than in set theory. Type theory has thus
emerged as an important framework for *automating* the
handling of logic in both mathematics and computer science.

For computer scientists, it is also the main foundation for
functional programming, the theory of programming languages,
and for formal verification of software correctness, which
is vitally important when ultra-high levels of confidence
in the correctness of code is required (e.g., for security).

This very tool and language that you're using now, the Lean
prover, is based on type theory. It's am example of what's
known in the business as a proof assistant. But you can just
think of it for now as a really cool tool in which you can
write both programs and logic, and that can help you to
construct "manageable" proofs. Through the magic of type
checking it then *automatically* determines whether a proof
is valid for a given proposition.

This technology holds the promise of eventually changing
the way that code is written and verified, and even the way
that mathematics work.
-/

/-
In this unit you've learned the following concepts:

* formal system
* proposition
* axiom
* inference rule
* truth judgment
* proof
* equality
* type judgment
* set theory and type theory foundations of mathematics
* type theory for automating mathematical logic and proof checking
-/


/-
Copyright (c) 2021 Lu-Ming Zhang. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Lu-Ming Zhang
-/
import data.matrix.block

/-!
# Symmetric matrices

This file contains the definition and basic results about symmetric matrices.

## Main definition

 * `matrix.is_symm `: a matrix `A : matrix n n α` is "symmetric" if `Aᵀ = A`.

## Tags

symm, symmetric, matrix
-/

variables {α β n m R : Type*}

namespace matrix

open_locale matrix

/-- A matrix `A : matrix n n α` is "symmetric" if `Aᵀ = A`. -/
def is_symm (A : matrix n n α) : Prop := Aᵀ = A

lemma is_symm.eq {A : matrix n n α} (h : A.is_symm) : Aᵀ = A := h

/-- A version of `matrix.ext_iff` that unfolds the `matrix.transpose`. -/
lemma is_symm.ext_iff {A : matrix n n α} : A.is_symm ↔ ∀ i j, A j i = A i j :=
matrix.ext_iff.symm

/-- A version of `matrix.ext` that unfolds the `matrix.transpose`. -/
@[ext]
lemma is_symm.ext {A : matrix n n α} : (∀ i j, A j i = A i j) → A.is_symm :=
matrix.ext

lemma is_symm.apply {A : matrix n n α} (h : A.is_symm) (i j : n) : A j i = A i j :=
is_symm.ext_iff.1 h i j

lemma is_symm_mul_transpose_self [fintype n] [comm_semiring α] (A : matrix n n α) :
  (A ⬝ Aᵀ).is_symm :=
transpose_mul _ _

lemma is_symm_transpose_mul_self [fintype n] [comm_semiring α] (A : matrix n n α) :
  (Aᵀ ⬝ A).is_symm :=
transpose_mul _ _

lemma is_symm_add_transpose_self [add_comm_semigroup α] (A : matrix n n α) :
  (A + Aᵀ).is_symm :=
add_comm _ _

lemma is_symm_transpose_add_self [add_comm_semigroup α] (A : matrix n n α) :
  (Aᵀ + A).is_symm :=
add_comm _ _

@[simp] lemma is_symm_zero [has_zero α] :
  (0 : matrix n n α).is_symm :=
transpose_zero

@[simp] lemma is_symm_one [decidable_eq n] [has_zero α] [has_one α] :
  (1 : matrix n n α).is_symm :=
transpose_one

@[simp] lemma is_symm.map {A : matrix n n α} (h : A.is_symm) (f : α → β) :
  (A.map f).is_symm :=
transpose_map.symm.trans (h.symm ▸ rfl)

@[simp] lemma is_symm.transpose {A : matrix n n α} (h : A.is_symm) :
  Aᵀ.is_symm :=
congr_arg _ h

@[simp] lemma is_symm.conj_transpose [has_star α] {A : matrix n n α} (h : A.is_symm) :
  Aᴴ.is_symm :=
h.transpose.map _

@[simp] lemma is_symm.neg [has_neg α] {A : matrix n n α} (h : A.is_symm) :
  (-A).is_symm :=
(transpose_neg _).trans (congr_arg _ h)

@[simp] lemma is_symm.add {A B : matrix n n α} [has_add α] (hA : A.is_symm) (hB : B.is_symm) :
  (A + B).is_symm :=
(transpose_add _ _).trans (hA.symm ▸ hB.symm ▸ rfl)

@[simp] lemma is_symm.sub {A B : matrix n n α} [has_sub α] (hA : A.is_symm) (hB : B.is_symm) :
  (A - B).is_symm :=
(transpose_sub _ _).trans (hA.symm ▸ hB.symm ▸ rfl)

@[simp] lemma is_symm.smul [has_smul R α] {A : matrix n n α} (h : A.is_symm) (k : R) :
  (k • A).is_symm :=
(transpose_smul _ _).trans (congr_arg _ h)

@[simp] lemma is_symm.submatrix {A : matrix n n α} (h : A.is_symm) (f : m → n) :
  (A.submatrix f f).is_symm :=
(transpose_submatrix _ _ _).trans (h.symm ▸ rfl)

/-- The diagonal matrix `diagonal v` is symmetric. -/
@[simp] lemma is_symm_diagonal [decidable_eq n] [has_zero α] (v : n → α) :
  (diagonal v).is_symm :=
diagonal_transpose _

/-- A block matrix `A.from_blocks B C D` is symmetric,
    if `A` and `D` are symmetric and `Bᵀ = C`. -/
lemma is_symm.from_blocks
  {A : matrix m m α} {B : matrix m n α} {C : matrix n m α} {D : matrix n n α}
  (hA : A.is_symm) (hBC : Bᵀ = C) (hD : D.is_symm) :
  (A.from_blocks B C D).is_symm :=
begin
  have hCB : Cᵀ = B, {rw ← hBC, simp},
  unfold matrix.is_symm,
  rw from_blocks_transpose,
  congr;
  assumption
end

/-- This is the `iff` version of `matrix.is_symm.from_blocks`. -/
lemma is_symm_from_blocks_iff
  {A : matrix m m α} {B : matrix m n α} {C : matrix n m α} {D : matrix n n α} :
  (A.from_blocks B C D).is_symm ↔ A.is_symm ∧ Bᵀ = C ∧ Cᵀ = B ∧ D.is_symm :=
⟨λ h, ⟨(congr_arg to_blocks₁₁ h : _), (congr_arg to_blocks₂₁ h : _),
       (congr_arg to_blocks₁₂ h : _), (congr_arg to_blocks₂₂ h : _)⟩,
 λ ⟨hA, hBC, hCB, hD⟩, is_symm.from_blocks hA hBC hD⟩

end matrix


/-
Copyright (c) 2021 Heather Macbeth. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Heather Macbeth, David Loeffler

! This file was ported from Lean 3 source module analysis.fourier.add_circle
! leanprover-community/mathlib commit 46b633fd842bef9469441c0209906f6dddd2b4f5
! Please do not edit these lines, except to modify the commit id
! if you have ported upstream changes.
-/
import Mathbin.Analysis.SpecialFunctions.Complex.Circle
import Mathbin.Topology.Instances.AddCircle
import Mathbin.Analysis.InnerProductSpace.L2Space
import Mathbin.MeasureTheory.Function.ContinuousMapDense
import Mathbin.MeasureTheory.Function.L2Space
import Mathbin.MeasureTheory.Group.Integration
import Mathbin.MeasureTheory.Integral.Periodic
import Mathbin.Topology.ContinuousFunction.StoneWeierstrass

/-!

# Fourier analysis on the additive circle

This file contains basic results on Fourier series for functions on the additive circle
`add_circle T = ℝ / ℤ • T`.

## Main definitions

* `haar_add_circle`, Haar measure on `add_circle T`, normalized to have total measure `1`. (Note
  that this is not the same normalisation as the standard measure defined in `integral.periodic`,
  so we do not declare it as a `measure_space` instance, to avoid confusion.)
* for `n : ℤ`, `fourier n` is the monomial `λ x, exp (2 π i n x / T)`, bundled as a continuous map
  from `add_circle T` to `ℂ`.
* `fourier_basis` is the Hilbert basis of `Lp ℂ 2 haar_add_circle` given by the images of the
  monomials `fourier n`.
* `fourier_coeff f n`, for `f : add_circle T → E` (with `E` a complete normed `ℂ`-vector space), is
  the `n`-th Fourier coefficient of `f`, defined as an integral over `add_circle T`. The lemma
  `fourier_coeff_eq_interval_integral` expresses this as an integral over `[a, a + T]` for any real
  `a`.
* `fourier_coeff_on`, for `f : ℝ → E` and `a < b` reals, is the `n`-th Fourier
  coefficient of the unique periodic function of period `b - a` which agrees with `f` on `(a, b]`.
  The lemma `fourier_coeff_on_eq_integral` expresses this as an integral over `[a, b]`.

## Main statements

The theorem `span_fourier_closure_eq_top` states that the span of the monomials `fourier n` is
dense in `C(add_circle T, ℂ)`, i.e. that its `submodule.topological_closure` is `⊤`.  This follows
from the Stone-Weierstrass theorem after checking that the span is a subalgebra, is closed under
conjugation, and separates points.

Using this and general theory on approximation of Lᵖ functions by continuous functions, we deduce
(`span_fourier_Lp_closure_eq_top`) that for any `1 ≤ p < ∞`, the span of the Fourier monomials is
dense in the Lᵖ space of `add_circle T`. For `p = 2` we show (`orthonormal_fourier`) that the
monomials are also orthonormal, so they form a Hilbert basis for L², which is named as
`fourier_basis`; in particular, for `L²` functions `f`, the Fourier series of `f` converges to `f`
in the `L²` topology (`has_sum_fourier_series_L2`). Parseval's identity, `tsum_sq_fourier_coeff`, is
a direct consequence.

For continuous maps `f : add_circle T → ℂ`, the theorem
`continuous_map.has_sum_fourier_series_of_summable` states that if the sequence of Fourier
coefficients of `f` is summable, then the Fourier series `∑ (i:ℤ), f.fourier_coeff i * fourier i`
converges to `f` in the uniform-convergence topology of `C(add_circle T, ℂ)`.
-/

noncomputable section

open ENNReal ComplexConjugate Real

open TopologicalSpace ContinuousMap MeasureTheory MeasureTheory.Measure Algebra Submodule Set

variable {T : ℝ}

namespace AddCircle

/-! ### Map from `add_circle` to `circle` -/

theorem scaled_exp_map_periodic : Function.Periodic (fun x => expMapCircle (2 * π / T * x)) T :=
  by
  -- The case T = 0 is not interesting, but it is true, so we prove it to save hypotheses
  rcases eq_or_ne T 0 with (rfl | hT)
  · intro x
    simp
  · intro x
    simp_rw [mul_add]
    rw [div_mul_cancel _ hT, periodic_expMapCircle]
#align add_circle.scaled_exp_map_periodic AddCircle.scaled_exp_map_periodic

/-- The canonical map `λ x, exp (2 π i x / T)` from `ℝ / ℤ • T` to the unit circle in `ℂ`.
If `T = 0` we understand this as the constant function 1. -/
def toCircle : AddCircle T → circle :=
  (@scaled_exp_map_periodic T).lift
#align add_circle.to_circle AddCircle.toCircle

theorem toCircle_add (x : AddCircle T) (y : AddCircle T) :
    toCircle (x + y) = toCircle x * toCircle y :=
  by
  induction x using QuotientAddGroup.induction_on'
  induction y using QuotientAddGroup.induction_on'
  simp_rw [← QuotientAddGroup.mk_add, to_circle, Function.Periodic.lift_coe, mul_add,
    expMapCircle_add]
#align add_circle.to_circle_add AddCircle.toCircle_add

theorem continuous_toCircle : Continuous (@toCircle T) :=
  continuous_coinduced_dom.mpr (expMapCircle.Continuous.comp <| continuous_const.mul continuous_id')
#align add_circle.continuous_to_circle AddCircle.continuous_toCircle

theorem injective_toCircle (hT : T ≠ 0) : Function.Injective (@toCircle T) :=
  by
  intro a b h
  induction a using QuotientAddGroup.induction_on'
  induction b using QuotientAddGroup.induction_on'
  simp_rw [to_circle, Function.Periodic.lift_coe] at h
  obtain ⟨m, hm⟩ := exp_map_circle_eq_exp_map_circle.mp h.symm
  simp_rw [QuotientAddGroup.eq, AddSubgroup.mem_zmultiples_iff, zsmul_eq_mul]
  use m
  field_simp [real.two_pi_pos.ne']  at hm
  rw [← mul_right_inj' real.two_pi_pos.ne']
  linarith
#align add_circle.injective_to_circle AddCircle.injective_toCircle

/-! ### Measure on `add_circle T`

In this file we use the Haar measure on `add_circle T` normalised to have total measure 1 (which is
**not** the same as the standard measure defined in `topology.instances.add_circle`). -/

variable [hT : Fact (0 < T)]

include hT

/-- Haar measure on the additive circle, normalised to have total measure 1. -/
def haarAddCircle : Measure (AddCircle T) :=
  add_haar_measure ⊤deriving IsAddHaarMeasure
#align add_circle.haar_add_circle AddCircle.haarAddCircle

instance : IsProbabilityMeasure (@haarAddCircle T _) :=
  IsProbabilityMeasure.mk add_haar_measure_self

theorem volume_eq_smul_haarAddCircle :
    (volume : Measure (AddCircle T)) = ENNReal.ofReal T • haarAddCircle :=
  rfl
#align add_circle.volume_eq_smul_haar_add_circle AddCircle.volume_eq_smul_haarAddCircle

end AddCircle

open AddCircle

section Monomials

/-- The family of exponential monomials `λ x, exp (2 π i n x / T)`, parametrized by `n : ℤ` and
considered as bundled continuous maps from `ℝ / ℤ • T` to `ℂ`. -/
def fourier (n : ℤ) : C(AddCircle T, ℂ)
    where
  toFun x := toCircle (n • x)
  continuous_toFun := continuous_induced_dom.comp <| continuous_toCircle.comp <| continuous_zsmul _
#align fourier fourier

@[simp]
theorem fourier_apply {n : ℤ} {x : AddCircle T} : fourier n x = toCircle (n • x) :=
  rfl
#align fourier_apply fourier_apply

@[simp]
theorem fourier_coe_apply {n : ℤ} {x : ℝ} :
    fourier n (x : AddCircle T) = Complex.exp (2 * π * Complex.I * n * x / T) :=
  by
  rw [fourier_apply, ← QuotientAddGroup.mk_zsmul, to_circle, Function.Periodic.lift_coe,
    expMapCircle_apply, Complex.ofReal_mul, Complex.ofReal_div, Complex.ofReal_mul, zsmul_eq_mul,
    Complex.ofReal_mul, Complex.ofReal_int_cast, Complex.ofReal_bit0, Complex.ofReal_one]
  congr 1; ring
#align fourier_coe_apply fourier_coe_apply

@[simp]
theorem fourier_zero {x : AddCircle T} : fourier 0 x = 1 :=
  by
  induction x using QuotientAddGroup.induction_on'
  simp only [fourier_coe_apply, algebraMap.coe_zero, MulZeroClass.mul_zero, MulZeroClass.zero_mul,
    zero_div, Complex.exp_zero]
#align fourier_zero fourier_zero

@[simp]
theorem fourier_eval_zero (n : ℤ) : fourier n (0 : AddCircle T) = 1 := by
  rw [← QuotientAddGroup.mk_zero, fourier_coe_apply, Complex.ofReal_zero, MulZeroClass.mul_zero,
    zero_div, Complex.exp_zero]
#align fourier_eval_zero fourier_eval_zero

@[simp]
theorem fourier_one {x : AddCircle T} : fourier 1 x = toCircle x := by rw [fourier_apply, one_zsmul]
#align fourier_one fourier_one

@[simp]
theorem fourier_neg {n : ℤ} {x : AddCircle T} : fourier (-n) x = conj (fourier n x) :=
  by
  induction x using QuotientAddGroup.induction_on'
  simp_rw [fourier_apply, to_circle, ← QuotientAddGroup.mk_zsmul, Function.Periodic.lift_coe, ←
    coe_inv_circle_eq_conj, ← expMapCircle_neg, neg_smul, mul_neg]
#align fourier_neg fourier_neg

@[simp]
theorem fourier_add {m n : ℤ} {x : AddCircle T} : fourier (m + n) x = fourier m x * fourier n x :=
  by simp_rw [fourier_apply, add_zsmul, to_circle_add, coe_mul_unitSphere]
#align fourier_add fourier_add

theorem fourier_norm [Fact (0 < T)] (n : ℤ) : ‖@fourier T n‖ = 1 :=
  by
  rw [ContinuousMap.norm_eq_supᵢ_norm]
  have : ∀ x : AddCircle T, ‖fourier n x‖ = 1 := fun x => abs_coe_circle _
  simp_rw [this]
  exact @csupᵢ_const _ _ _ Zero.nonempty _
#align fourier_norm fourier_norm

/-- For `n ≠ 0`, a translation by `T / 2 / n` negates the function `fourier n`. -/
theorem fourier_add_half_inv_index {n : ℤ} (hn : n ≠ 0) (hT : 0 < T) (x : AddCircle T) :
    fourier n (x + (T / 2 / n : ℝ)) = -fourier n x :=
  by
  rw [fourier_apply, zsmul_add, ← QuotientAddGroup.mk_zsmul, to_circle_add, coe_mul_unitSphere]
  have : (n : ℂ) ≠ 0 := by simpa using hn
  have : (@to_circle T (n • (T / 2 / n) : ℝ) : ℂ) = -1 :=
    by
    rw [zsmul_eq_mul, to_circle, Function.Periodic.lift_coe, expMapCircle_apply]
    replace hT := complex.of_real_ne_zero.mpr hT.ne'
    convert Complex.exp_pi_mul_i using 3
    field_simp
    ring
  rw [this]
  simp
#align fourier_add_half_inv_index fourier_add_half_inv_index

/-- The subalgebra of `C(add_circle T, ℂ)` generated by `fourier n` for `n ∈ ℤ` . -/
def fourierSubalgebra : Subalgebra ℂ C(AddCircle T, ℂ) :=
  Algebra.adjoin ℂ (range fourier)
#align fourier_subalgebra fourierSubalgebra

/-- The subalgebra of `C(add_circle T, ℂ)` generated by `fourier n` for `n ∈ ℤ` is in fact the
linear span of these functions. -/
theorem fourierSubalgebra_coe : (@fourierSubalgebra T).toSubmodule = span ℂ (range fourier) :=
  by
  apply adjoin_eq_span_of_subset
  refine' subset.trans _ Submodule.subset_span
  intro x hx
  apply Submonoid.closure_induction hx (fun _ => id) ⟨0, _⟩
  · rintro _ _ ⟨m, rfl⟩ ⟨n, rfl⟩
    refine' ⟨m + n, _⟩
    ext1 z
    exact fourier_add
  · ext1 z
    exact fourier_zero
#align fourier_subalgebra_coe fourierSubalgebra_coe

/-- The subalgebra of `C(add_circle T, ℂ)` generated by `fourier n` for `n ∈ ℤ` is invariant under
complex conjugation. -/
theorem fourierSubalgebra_conj_invariant :
    ConjInvariantSubalgebra ((@fourierSubalgebra T).restrictScalars ℝ) :=
  by
  apply subalgebra_conj_invariant
  rintro _ ⟨n, rfl⟩
  exact ⟨-n, ext fun _ => fourier_neg⟩
#align fourier_subalgebra_conj_invariant fourierSubalgebra_conj_invariant

variable [hT : Fact (0 < T)]

include hT

/-- The subalgebra of `C(add_circle T, ℂ)` generated by `fourier n` for `n ∈ ℤ`
separates points. -/
theorem fourierSubalgebra_separatesPoints : (@fourierSubalgebra T).SeparatesPoints :=
  by
  intro x y hxy
  refine' ⟨_, ⟨fourier 1, subset_adjoin ⟨1, rfl⟩, rfl⟩, _⟩
  dsimp only; rw [fourier_one, fourier_one]
  contrapose! hxy
  rw [Subtype.coe_inj] at hxy
  exact injective_to_circle hT.elim.ne' hxy
#align fourier_subalgebra_separates_points fourierSubalgebra_separatesPoints

/-- The subalgebra of `C(add_circle T, ℂ)` generated by `fourier n` for `n ∈ ℤ` is dense. -/
theorem fourierSubalgebra_closure_eq_top : (@fourierSubalgebra T).topologicalClosure = ⊤ :=
  ContinuousMap.subalgebra_isROrC_topologicalClosure_eq_top_of_separatesPoints fourierSubalgebra
    fourierSubalgebra_separatesPoints fourierSubalgebra_conj_invariant
#align fourier_subalgebra_closure_eq_top fourierSubalgebra_closure_eq_top

/-- The linear span of the monomials `fourier n` is dense in `C(add_circle T, ℂ)`. -/
theorem span_fourier_closure_eq_top : (span ℂ (range <| @fourier T)).topologicalClosure = ⊤ :=
  by
  rw [← fourierSubalgebra_coe]
  exact congr_arg Subalgebra.toSubmodule fourierSubalgebra_closure_eq_top
#align span_fourier_closure_eq_top span_fourier_closure_eq_top

/-- The family of monomials `fourier n`, parametrized by `n : ℤ` and considered as
elements of the `Lp` space of functions `add_circle T → ℂ`. -/
abbrev fourierLp (p : ℝ≥0∞) [Fact (1 ≤ p)] (n : ℤ) : lp ℂ p (@haarAddCircle T hT) :=
  toLp p haarAddCircle ℂ (fourier n)
#align fourier_Lp fourierLp

theorem coeFn_fourierLp (p : ℝ≥0∞) [Fact (1 ≤ p)] (n : ℤ) :
    @fourierLp T hT p _ n =ᵐ[haarAddCircle] fourier n :=
  coeFn_toLp haarAddCircle (fourier n)
#align coe_fn_fourier_Lp coeFn_fourierLp

/-- For each `1 ≤ p < ∞`, the linear span of the monomials `fourier n` is dense in
`Lp ℂ p haar_circle`. -/
theorem span_fourierLp_closure_eq_top {p : ℝ≥0∞} [Fact (1 ≤ p)] (hp : p ≠ ∞) :
    (span ℂ (range (@fourierLp T _ p _))).topologicalClosure = ⊤ :=
  by
  convert(ContinuousMap.toLp_denseRange ℂ hp (@haar_add_circle T hT)
          ℂ).topologicalClosure_map_submodule
      span_fourier_closure_eq_top
  rw [map_span, range_comp]
  simp only [ContinuousLinearMap.coe_coe]
#align span_fourier_Lp_closure_eq_top span_fourierLp_closure_eq_top

/-- The monomials `fourier n` are an orthonormal set with respect to normalised Haar measure. -/
theorem orthonormal_fourier : Orthonormal ℂ (@fourierLp T _ 2 _) :=
  by
  rw [orthonormal_iff_ite]
  intro i j
  rw [continuous_map.inner_to_Lp (@haar_add_circle T hT) (fourier i) (fourier j)]
  simp_rw [← fourier_neg, ← fourier_add]
  split_ifs
  · simp_rw [h, neg_add_self]
    have : ⇑(@fourier T 0) = (fun x => 1 : AddCircle T → ℂ) :=
      by
      ext1
      exact fourier_zero
    rw [this, integral_const, measure_univ, ENNReal.one_toReal, Complex.real_smul,
      Complex.ofReal_one, mul_one]
  have hij : -i + j ≠ 0 := by
    rw [add_comm]
    exact sub_ne_zero.mpr (Ne.symm h)
  convert integral_eq_zero_of_add_right_eq_neg (fourier_add_half_inv_index hij hT.elim)
  exact IsAddLeftInvariant.is_add_right_invariant
#align orthonormal_fourier orthonormal_fourier

end Monomials

section ScopeHT

-- everything from here on needs `0 < T`
variable [hT : Fact (0 < T)]

include hT

section fourierCoeff

variable {E : Type} [NormedAddCommGroup E] [NormedSpace ℂ E] [CompleteSpace E]

/-- The `n`-th Fourier coefficient of a function `add_circle T → E`, for `E` a complete normed
`ℂ`-vector space, defined as the integral over `add_circle T` of `fourier (-n) t • f t`. -/
def fourierCoeff (f : AddCircle T → E) (n : ℤ) : E :=
  ∫ t : AddCircle T, fourier (-n) t • f t ∂haarAddCircle
#align fourier_coeff fourierCoeff

/-- The Fourier coefficients of a function on `add_circle T` can be computed as an integral
over `[a, a + T]`, for any real `a`. -/
theorem fourierCoeff_eq_intervalIntegral (f : AddCircle T → E) (n : ℤ) (a : ℝ) :
    fourierCoeff f n = (1 / T) • ∫ x in a..a + T, @fourier T (-n) x • f x :=
  by
  have : ∀ x : ℝ, @fourier T (-n) x • f x = (fun z : AddCircle T => @fourier T (-n) z • f z) x :=
    by
    intro x
    rfl
  simp_rw [this]
  rw [fourierCoeff, AddCircle.intervalIntegral_preimage T a, volume_eq_smul_haar_add_circle,
    integral_smul_measure, ENNReal.toReal_ofReal hT.out.le, ← smul_assoc, smul_eq_mul,
    one_div_mul_cancel hT.out.ne', one_smul]
#align fourier_coeff_eq_interval_integral fourierCoeff_eq_intervalIntegral

theorem fourierCoeff.const_smul (f : AddCircle T → E) (c : ℂ) (n : ℤ) :
    fourierCoeff (c • f) n = c • fourierCoeff f n := by
  simp_rw [fourierCoeff, Pi.smul_apply, ← smul_assoc, smul_eq_mul, mul_comm, ← smul_eq_mul,
    smul_assoc, integral_smul]
#align fourier_coeff.const_smul fourierCoeff.const_smul

theorem fourierCoeff.const_mul (f : AddCircle T → ℂ) (c : ℂ) (n : ℤ) :
    fourierCoeff (fun x => c * f x) n = c * fourierCoeff f n :=
  fourierCoeff.const_smul f c n
#align fourier_coeff.const_mul fourierCoeff.const_mul

omit hT

/-- For a function on `ℝ`, the Fourier coefficients of `f` on `[a, b]` are defined as the
Fourier coefficients of the unique periodic function agreeing with `f` on `Ioc a b`. -/
def fourierCoeffOn {a b : ℝ} (hab : a < b) (f : ℝ → E) (n : ℤ) : E :=
  haveI := Fact.mk (by linarith : 0 < b - a)
  fourierCoeff (AddCircle.liftIoc (b - a) a f) n
#align fourier_coeff_on fourierCoeffOn

theorem fourierCoeffOn_eq_integral {a b : ℝ} (f : ℝ → E) (n : ℤ) (hab : a < b) :
    fourierCoeffOn hab f n =
      (1 / (b - a)) • ∫ x in a..b, fourier (-n) (x : AddCircle (b - a)) • f x :=
  by
  rw [fourierCoeffOn, fourierCoeff_eq_intervalIntegral _ _ a]
  congr 1
  rw [add_sub, add_sub_cancel']
  simp_rw [intervalIntegral.integral_of_le hab.le]
  refine' set_integral_congr measurableSet_Ioc fun x hx => _
  dsimp only
  rwa [lift_Ioc_coe_apply]
  rwa [add_sub, add_sub_cancel']
#align fourier_coeff_on_eq_integral fourierCoeffOn_eq_integral

theorem fourierCoeffOn.const_smul {a b : ℝ} (f : ℝ → E) (c : ℂ) (n : ℤ) (hab : a < b) :
    fourierCoeffOn hab (c • f) n = c • fourierCoeffOn hab f n := by apply fourierCoeff.const_smul
#align fourier_coeff_on.const_smul fourierCoeffOn.const_smul

theorem fourierCoeffOn.const_mul {a b : ℝ} (f : ℝ → ℂ) (c : ℂ) (n : ℤ) (hab : a < b) :
    fourierCoeffOn hab (fun x => c * f x) n = c * fourierCoeffOn hab f n :=
  fourierCoeffOn.const_smul _ _ _ _
#align fourier_coeff_on.const_mul fourierCoeffOn.const_mul

include hT

theorem fourierCoeff_liftIoc_eq {a : ℝ} (f : ℝ → ℂ) (n : ℤ) :
    fourierCoeff (AddCircle.liftIoc T a f) n = fourierCoeffOn (lt_add_of_pos_right a hT.out) f n :=
  by
  rw [fourierCoeffOn_eq_integral, fourierCoeff_eq_intervalIntegral, add_sub_cancel' a T]
  congr 1
  refine' intervalIntegral.integral_congr_ae (ae_of_all _ fun x hx => _)
  rw [lift_Ioc_coe_apply]
  rwa [uIoc_of_le (lt_add_of_pos_right a hT.out).le] at hx
#align fourier_coeff_lift_Ioc_eq fourierCoeff_liftIoc_eq

theorem fourierCoeff_liftIco_eq {a : ℝ} (f : ℝ → ℂ) (n : ℤ) :
    fourierCoeff (AddCircle.liftIco T a f) n = fourierCoeffOn (lt_add_of_pos_right a hT.out) f n :=
  by
  rw [fourierCoeffOn_eq_integral, fourierCoeff_eq_intervalIntegral _ _ a, add_sub_cancel' a T]
  congr 1
  simp_rw [intervalIntegral.integral_of_le (lt_add_of_pos_right a hT.out).le,
    integral_Ioc_eq_integral_Ioo]
  refine' set_integral_congr measurableSet_Ioo fun x hx => _
  dsimp only
  rw [lift_Ico_coe_apply (Ioo_subset_Ico_self hx)]
#align fourier_coeff_lift_Ico_eq fourierCoeff_liftIco_eq

end fourierCoeff

section FourierL2

/-- We define `fourier_basis` to be a `ℤ`-indexed Hilbert basis for `Lp ℂ 2 haar_add_circle`,
which by definition is an isometric isomorphism from `Lp ℂ 2 haar_add_circle` to `ℓ²(ℤ, ℂ)`. -/
def fourierBasis : HilbertBasis ℤ ℂ (lp ℂ 2 <| @haarAddCircle T hT) :=
  HilbertBasis.mk orthonormal_fourier (span_fourierLp_closure_eq_top (by norm_num)).ge
#align fourier_basis fourierBasis

/-- The elements of the Hilbert basis `fourier_basis` are the functions `fourier_Lp 2`, i.e. the
monomials `fourier n` on the circle considered as elements of `L²`. -/
@[simp]
theorem coe_fourierBasis : ⇑(@fourierBasis _ hT) = fourierLp 2 :=
  HilbertBasis.coe_mk _ _
#align coe_fourier_basis coe_fourierBasis

/-- Under the isometric isomorphism `fourier_basis` from `Lp ℂ 2 haar_circle` to `ℓ²(ℤ, ℂ)`, the
`i`-th coefficient is `fourier_coeff f i`, i.e., the integral over `add_circle T` of
`λ t, fourier (-i) t * f t` with respect to the Haar measure of total mass 1. -/
theorem fourierBasis_repr (f : lp ℂ 2 <| @haarAddCircle T hT) (i : ℤ) :
    fourierBasis.repr f i = fourierCoeff f i :=
  by
  trans ∫ t : AddCircle T, conj ((@fourierLp T hT 2 _ i : AddCircle T → ℂ) t) * f t ∂haar_add_circle
  · simp [fourier_basis.repr_apply_apply f i, MeasureTheory.L2.inner_def]
  · apply integral_congr_ae
    filter_upwards [coeFn_fourierLp 2 i]with _ ht
    rw [ht, ← fourier_neg, smul_eq_mul]
#align fourier_basis_repr fourierBasis_repr

/-- The Fourier series of an `L2` function `f` sums to `f`, in the `L²` space of `add_circle T`. -/
theorem hasSum_fourier_series_L2 (f : lp ℂ 2 <| @haarAddCircle T hT) :
    HasSum (fun i => fourierCoeff f i • fourierLp 2 i) f :=
  by
  simp_rw [← fourierBasis_repr]
  simpa using HilbertBasis.hasSum_repr fourierBasis f
#align has_sum_fourier_series_L2 hasSum_fourier_series_L2

/-- **Parseval's identity**: for an `L²` function `f` on `add_circle T`, the sum of the squared
norms of the Fourier coefficients equals the `L²` norm of `f`. -/
theorem tsum_sq_fourierCoeff (f : lp ℂ 2 <| @haarAddCircle T hT) :
    (∑' i : ℤ, ‖fourierCoeff f i‖ ^ 2) = ∫ t : AddCircle T, ‖f t‖ ^ 2 ∂haarAddCircle :=
  by
  simp_rw [← fourierBasis_repr]
  have H₁ : ‖fourier_basis.repr f‖ ^ 2 = ∑' i, ‖fourier_basis.repr f i‖ ^ 2 :=
    by
    exact_mod_cast lp.norm_rpow_eq_tsum _ (fourier_basis.repr f)
    norm_num
  have H₂ : ‖fourier_basis.repr f‖ ^ 2 = ‖f‖ ^ 2 := by simp
  have H₃ := congr_arg IsROrC.re (@L2.inner_def (AddCircle T) ℂ ℂ _ _ _ _ _ f f)
  rw [← integral_re] at H₃
  · simp only [← norm_sq_eq_inner] at H₃
    rw [← H₁, H₂, H₃]
  · exact L2.integrable_inner f f
#align tsum_sq_fourier_coeff tsum_sq_fourierCoeff

end FourierL2

section Convergence

variable (f : C(AddCircle T, ℂ))

theorem fourierCoeff_toLp (n : ℤ) : fourierCoeff (toLp 2 haarAddCircle ℂ f) n = fourierCoeff f n :=
  integral_congr_ae
    (Filter.EventuallyEq.mul (Filter.eventually_of_forall (by tauto))
      (ContinuousMap.coeFn_toAeEqFun haarAddCircle f))
#align fourier_coeff_to_Lp fourierCoeff_toLp

variable {f}

/-- If the sequence of Fourier coefficients of `f` is summable, then the Fourier series converges
uniformly to `f`. -/
theorem hasSum_fourier_series_of_summable (h : Summable (fourierCoeff f)) :
    HasSum (fun i => fourierCoeff f i • fourier i) f :=
  by
  have sum_L2 := hasSum_fourier_series_L2 (to_Lp 2 haar_add_circle ℂ f)
  simp_rw [fourierCoeff_toLp] at sum_L2
  refine' ContinuousMap.hasSum_of_hasSum_lp (summable_of_summable_norm _) sum_L2
  simp_rw [norm_smul, fourier_norm, mul_one, summable_norm_iff]
  exact h
#align has_sum_fourier_series_of_summable hasSum_fourier_series_of_summable

/-- If the sequence of Fourier coefficients of `f` is summable, then the Fourier series of `f`
converges everywhere pointwise to `f`. -/
theorem has_pointwise_sum_fourier_series_of_summable (h : Summable (fourierCoeff f))
    (x : AddCircle T) : HasSum (fun i => fourierCoeff f i • fourier i x) (f x) :=
  (ContinuousMap.evalClm ℂ x).HasSum (hasSum_fourier_series_of_summable h)
#align has_pointwise_sum_fourier_series_of_summable has_pointwise_sum_fourier_series_of_summable

end Convergence

end ScopeHT

section deriv

open Complex intervalIntegral

open Interval

variable (T)

theorem hasDerivAt_fourier (n : ℤ) (x : ℝ) :
    HasDerivAt (fun y : ℝ => fourier n (y : AddCircle T))
      (2 * π * I * n / T * fourier n (x : AddCircle T)) x :=
  by
  simp_rw [fourier_coe_apply]
  refine' (_ : HasDerivAt (fun y => exp (2 * π * I * n * y / T)) _ _).comp_of_real
  rw [(fun α β => by ring : ∀ α β : ℂ, α * exp β = exp β * α)]
  refine' (has_deriv_at_exp _).comp x _
  convert hasDerivAt_mul_const (2 * ↑π * I * ↑n / T)
  ext1 y; ring
#align has_deriv_at_fourier hasDerivAt_fourier

theorem hasDerivAt_fourier_neg (n : ℤ) (x : ℝ) :
    HasDerivAt (fun y : ℝ => fourier (-n) (y : AddCircle T))
      (-2 * π * I * n / T * fourier (-n) (x : AddCircle T)) x :=
  by simpa using hasDerivAt_fourier T (-n) x
#align has_deriv_at_fourier_neg hasDerivAt_fourier_neg

variable {T}

theorem has_antideriv_at_fourier_neg (hT : Fact (0 < T)) {n : ℤ} (hn : n ≠ 0) (x : ℝ) :
    HasDerivAt (fun y : ℝ => (T : ℂ) / (-2 * π * I * n) * fourier (-n) (y : AddCircle T))
      (fourier (-n) (x : AddCircle T)) x :=
  by
  convert(hasDerivAt_fourier_neg T n x).div_const (-2 * π * I * n / T) using 1
  · ext1 y
    rw [div_div_eq_mul_div]
    ring
  · rw [mul_div_cancel_left]
    simp only [Ne.def, div_eq_zero_iff, neg_eq_zero, mul_eq_zero, bit0_eq_zero, one_ne_zero,
      of_real_eq_zero, false_or_iff, Int.cast_eq_zero, not_or]
    exact ⟨⟨⟨Real.pi_ne_zero, I_ne_zero⟩, hn⟩, hT.out.ne'⟩
#align has_antideriv_at_fourier_neg has_antideriv_at_fourier_neg

/-- Express Fourier coefficients of `f` on an interval in terms of those of its derivative. -/
theorem fourierCoeffOn_of_hasDerivAt {a b : ℝ} (hab : a < b) {f f' : ℝ → ℂ} {n : ℤ} (hn : n ≠ 0)
    (hf : ∀ x, x ∈ [a, b] → HasDerivAt f (f' x) x) (hf' : IntervalIntegrable f' volume a b) :
    fourierCoeffOn hab f n =
      1 / (-2 * π * I * n) *
        (fourier (-n) (a : AddCircle (b - a)) * (f b - f a) - (b - a) * fourierCoeffOn hab f' n) :=
  by
  rw [← of_real_sub]
  have hT : Fact (0 < b - a) := ⟨by linarith⟩
  simp_rw [fourierCoeffOn_eq_integral, smul_eq_mul, real_smul, of_real_div, of_real_one]
  conv => pattern (occs := 1 2 3) fourier _ _ * _ <;> (rw [mul_comm])
  rw [integral_mul_deriv_eq_deriv_mul hf (fun x hx => has_antideriv_at_fourier_neg hT hn x) hf'
      (((map_continuous (fourier (-n))).comp (AddCircle.continuous_mk' _)).IntervalIntegrable _ _)]
  dsimp only
  have : ∀ u v w : ℂ, u * ((b - a : ℝ) / v * w) = (b - a : ℝ) / v * (u * w) :=
    by
    intros
    ring
  conv in intervalIntegral _ _ _ _ =>
    congr
    ext
    rw [this]
  rw [(by ring : ((b - a : ℝ) : ℂ) / (-2 * π * I * n) = ((b - a : ℝ) : ℂ) * (1 / (-2 * π * I * n)))]
  have s2 : (b : AddCircle (b - a)) = (a : AddCircle (b - a)) := by
    simpa using coe_add_period (b - a) a
  rw [s2, integral_const_mul, ← sub_mul, mul_sub, mul_sub]
  congr 1
  · conv_lhs => rw [mul_comm, mul_div, mul_one]
    rw [div_eq_iff (of_real_ne_zero.mpr hT.out.ne')]
    ring
  · ring
#align fourier_coeff_on_of_has_deriv_at fourierCoeffOn_of_hasDerivAt

end deriv


/-
Copyright (c) 2021 Anne Baanen. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Anne Baanen
-/
import data.matrix.basic
import linear_algebra.matrix.determinant
import linear_algebra.matrix.adjugate

/-!
# Matrices associated with non-degenerate bilinear forms

## Main definitions

* `matrix.nondegenerate A`: the proposition that when interpreted as a bilinear form, the matrix `A`
  is nondegenerate.

-/

namespace matrix

variables {m R A : Type*} [fintype m] [comm_ring R]

/-- A matrix `M` is nondegenerate if for all `v ≠ 0`, there is a `w ≠ 0` with `w ⬝ M ⬝ v ≠ 0`. -/
def nondegenerate (M : matrix m m R) :=
∀ v, (∀ w, matrix.dot_product v (mul_vec M w) = 0) → v = 0

/-- If `M` is nondegenerate and `w ⬝ M ⬝ v = 0` for all `w`, then `v = 0`. -/
lemma nondegenerate.eq_zero_of_ortho {M : matrix m m R} (hM : nondegenerate M)
  {v : m → R} (hv : ∀ w, matrix.dot_product v (mul_vec M w) = 0) : v = 0 :=
hM v hv

/-- If `M` is nondegenerate and `v ≠ 0`, then there is some `w` such that `w ⬝ M ⬝ v ≠ 0`. -/
lemma nondegenerate.exists_not_ortho_of_ne_zero {M : matrix m m R} (hM : nondegenerate M)
  {v : m → R} (hv : v ≠ 0) : ∃ w, matrix.dot_product v (mul_vec M w) ≠ 0 :=
not_forall.mp (mt hM.eq_zero_of_ortho hv)

variables [comm_ring A] [is_domain A]

/-- If `M` has a nonzero determinant, then `M` as a bilinear form on `n → A` is nondegenerate.

See also `bilin_form.nondegenerate_of_det_ne_zero'` and `bilin_form.nondegenerate_of_det_ne_zero`.
-/
theorem nondegenerate_of_det_ne_zero [decidable_eq m] {M : matrix m m A} (hM : M.det ≠ 0) :
  nondegenerate M :=
begin
  intros v hv,
  ext i,
  specialize hv (M.cramer (pi.single i 1)),
  refine (mul_eq_zero.mp _).resolve_right hM,
  convert hv,
  simp only [mul_vec_cramer M (pi.single i 1), dot_product, pi.smul_apply, smul_eq_mul],
  rw [finset.sum_eq_single i, pi.single_eq_same, mul_one],
  { intros j _ hj, simp [hj] },
  { intros, have := finset.mem_univ i, contradiction }
end

theorem eq_zero_of_vec_mul_eq_zero [decidable_eq m] {M : matrix m m A} (hM : M.det ≠ 0) {v : m → A}
  (hv : M.vec_mul v = 0) : v = 0 :=
(nondegenerate_of_det_ne_zero hM).eq_zero_of_ortho
  (λ w, by rw [dot_product_mul_vec, hv, zero_dot_product])

theorem eq_zero_of_mul_vec_eq_zero [decidable_eq m] {M : matrix m m A} (hM : M.det ≠ 0) {v : m → A}
  (hv : M.mul_vec v = 0) :
  v = 0 :=
eq_zero_of_vec_mul_eq_zero (by rwa det_transpose) ((vec_mul_transpose M v).trans hv)

end matrix


import catalan

open finset catalan

@[derive decidable_eq]
inductive binary_tree
| empty : binary_tree
| branch : binary_tree → binary_tree → binary_tree

namespace binary_tree

def vertices : binary_tree → ℕ
| empty := 0
| (branch l r) := (vertices l) + (vertices r) + 1

@[simp] def vertices_Empty : empty.vertices = 0 := rfl
@[simp] def vertices_Branch (l r : binary_tree) : (branch l r).vertices = l.vertices + r.vertices + 1 := rfl

lemma eq_Empty_of_vertices_zero {t : binary_tree} (h : t.vertices = 0) : t = empty :=
begin
  cases t with l r,
  { refl },
  { rw [vertices_Branch] at h, simpa only using h },
end

lemma eq_Branch_of_vertices_succ {n : ℕ} {t : binary_tree} (h : t.vertices = n.succ) :
  ∃ l r, t = branch l r :=
begin
  cases t with l r,
  { rw [vertices_Empty] at h, simpa only using h},
  { use [l, r] },
end

def all_trees : Π (n : ℕ), finset binary_tree
| 0 := ({ empty } : finset binary_tree)
| (n + 1) :=
  finset.bUnion (finset.nat.antidiagonal n) $
  λ p, if h : p.1 + p.2 = n then
    let h₁ : p.1 < n + 1 := nat.lt_succ_of_add_right_eq h in
    let h₂ : p.2 < n + 1 := nat.lt_succ_of_add_left_eq h in
      finset.image
      (λ i : binary_tree × binary_tree, branch i.1 i.2)
      (finset.product (all_trees p.1) (all_trees p.2))
    else ∅

@[simp] lemma all_trees_zero : all_trees 0 = ({ empty } : finset binary_tree) := by rw all_trees
@[simp] lemma all_trees_succ {n : ℕ} : all_trees n.succ =
  (nat.antidiagonal n).bUnion
    (λ p, finset.image (λ i : binary_tree × binary_tree, branch i.1 i.2) $ finset.product (all_trees p.1) (all_trees p.2)) :=
begin
  -- TODO: extract as bUnion_congr or something
  rw [all_trees],
  ext t,
  simp only [mem_bUnion, mem_image, dite_eq_ite, exists_prop, nat.mem_antidiagonal, prod.exists, mem_product],
  split,
  {
    rintro ⟨a, b, rfl, h⟩,
    use [a, b, rfl],
    simpa only [mem_image, exists_prop, if_pos rfl, prod.exists, mem_product] using h,
  }, {
    rintro ⟨a, b, rfl, h⟩,
    use [a, b, rfl],
    rw if_pos rfl,
    simpa only [mem_image, exists_prop, prod.exists, mem_product] using h,
  }
end

def mem_all_trees {t : binary_tree} : t ∈ all_trees t.vertices :=
begin
  induction t with a b iha ihb, {
    simp only [vertices_Empty, mem_singleton, all_trees_zero],
  }, {
    simp only [mem_bUnion, mem_image, exists_prop, vertices_Branch, exists_eq_right_right, exists_eq_right, all_trees_succ,
  nat.mem_antidiagonal, prod.exists, mem_product],
    use [a.vertices, b.vertices, rfl],
    exact ⟨iha, ihb⟩,
  }
end

def vertices_eq_of_mem_all_trees {t : binary_tree} {n : ℕ} (h : t ∈ all_trees n) : t.vertices = n :=
begin
  induction t with a b iha ihb generalizing n, {
    cases n, {
      exact vertices_Empty,
    }, {
      simp [all_trees] at h,
      rcases h with ⟨a, b, rfl, h⟩,
      simpa only [if_pos rfl, mem_image, if_true, exists_false] using h,
    }
  }, {
    cases n, {
      simpa using h,
    }, {
      simp [all_trees] at h,
      rcases h with ⟨x, y, rfl, h⟩,
      simp only [mem_image, exists_prop, if_true, exists_eq_right_right, eq_self_iff_true, exists_eq_right, prod.exists, mem_product] at h,
      rw [←iha h.1, ←ihb h.2],
      exact vertices_Branch _ _,
    }
  }
end
end binary_tree

open binary_tree

lemma all_trees_card_eq_catalan {n} : (all_trees n).card = catalan n :=
begin
  apply nat.strong_induction_on n,
  clear n,
  rintro n ih,
  cases n, {
    rw [catalan_zero, all_trees_zero, card_singleton],
  }, {
    simp only [catalan_succ, all_trees_succ],
    rw [card_bUnion], {
      apply sum_congr rfl,
      rintro ⟨a, b⟩ h,
      rw nat.mem_antidiagonal at h,
      rw [card_image_of_injective, card_product],
      {
        rw [ih _ $ nat.lt_succ_of_add_right_eq h],
        rw [ih _ $ nat.lt_succ_of_add_left_eq h],
      }, {
        rintro a b,
        simp only [and_imp],
        exact prod.ext,
      },
    }, {
      rintro x - y - hneq k hk,
      simp only [mem_image, exists_prop, inf_eq_inter, mem_inter, prod.exists, mem_product] at hk,
      rcases hk with ⟨⟨a1, b1, ha1, hc1⟩, ⟨a2, b2, ha2, rfl⟩⟩,
      rcases branch.inj hc1 with ⟨rfl, rfl⟩,
      apply hneq,
      ext, {
        rw [←vertices_eq_of_mem_all_trees ha1.1, ←vertices_eq_of_mem_all_trees ha2.1],
      }, {
        rw [←vertices_eq_of_mem_all_trees ha1.2, ←vertices_eq_of_mem_all_trees ha2.2],
      },
    }
  }
end

def sized_binary_tree (n : ℕ) := {t : binary_tree // vertices t = n}
@[simp] lemma sized_binary_tree_def {n : ℕ} : (sized_binary_tree n) = {t : binary_tree // vertices t = n} := rfl

instance {n : ℕ} : fintype (sized_binary_tree n) :=
  ⟨(all_trees n).subtype (λ t, t.vertices = n),
  begin
    rintro ⟨t, rfl⟩,
    rw mem_subtype,
    exact mem_all_trees,
  end⟩

lemma catalan_eq_TreeN_card {n : ℕ} : fintype.card (sized_binary_tree n) = catalan n :=
begin
  rw [←all_trees_card_eq_catalan],
  apply fintype.card_of_subtype,
  intro x,
  split, {
    exact vertices_eq_of_mem_all_trees,
  }, {
    rintro rfl,
    exact mem_all_trees,
  }
end


-- División de una suma
-- ====================

import tactic
import data.nat.modeq

-- 1ª demostración
example
  (a b c : ℕ)
  (hca : c ∣ a)
  (hba : c ∣ b)
  : (a+b)/c=a/c+b/c :=
begin
  cases le_or_gt c 0 with hc hc,
  { rw nat.le_zero_iff at hc,
    rw hc,
    simp only [nat.div_zero] },
  { rw [nat.add_div hc, if_neg, add_zero],
    rw [nat.mod_eq_zero_of_dvd hca, nat.mod_eq_zero_of_dvd hba, add_zero],
    exact hc.lt.not_le, },
end

-- 2ª demostración
example
  (a b c : ℕ)
  (hca : c ∣ a)
  (hba : c ∣ b)
  : (a+b)/c=a/c+b/c :=
if h : c = 0
then by simp [h]
else nat.add_div_eq_of_add_mod_lt
begin
  rw [nat.mod_eq_zero_of_dvd hca,
      nat.mod_eq_zero_of_dvd hba,
      zero_add],
  exact nat.pos_of_ne_zero h,
end

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

lemma add_div_of_dvd_right
  {a b c : ℤ}
  (H : c ∣ b)
  : (a + b) / c = a / c + b / c :=
begin
  by_cases h1 : c = 0,
  { simp [h1], },
  { cases H with k hk,
    rw hk,
    change c ≠ 0 at h1,
    rw [mul_comm c k,
        int.add_mul_div_right _ _ h1,
        ← zero_add (k * c),
        int.add_mul_div_right _ _ h1,
        int.zero_div,
        zero_add]},
end

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

example
  {a b c : ℤ}
  (H : c ∣ a) :
  (a + b) / c = a / c + b / c :=
by rw [add_comm,
       add_div_of_dvd_right H,
       add_comm]

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


-- Cuadrado_del_binomio.lean
-- Cuadrado del binomio.
-- José A. Alonso Jiménez <https://jaalonso.github.io>
-- Sevilla, 24-agosto-2022
-- ---------------------------------------------------------------------

-- ---------------------------------------------------------------------
-- Demostrar que si a y b son números reales, entonces
--    (a + b) * (a + b) = a * a + 2 * (a * b) + b * b
-- ---------------------------------------------------------------------

import data.real.basic

variables a b : ℝ

-- 1ª demostración
-- ===============

example :
  (a + b) * (a + b) = a * a + 2 * (a * b) + b * b :=
calc
  (a + b) * (a + b)
      = (a + b) * a + (a + b) * b       : by rw mul_add
  ... = a * a + b * a + (a + b) * b     : by rw add_mul
  ... = a * a + b * a + (a * b + b * b) : by rw add_mul
  ... = a * a + b * a + a * b + b * b   : by rw ← add_assoc
  ... = a * a + (b * a + a * b) + b * b : by rw add_assoc (a * a)
  ... = a * a + (a * b + a * b) + b * b : by rw mul_comm b a
  ... = a * a + 2 * (a * b) + b * b     : by rw ← two_mul

-- 2ª demostración
-- ===============

example :
  (a + b) * (a + b) = a * a + 2 * (a * b) + b * b :=
calc
  (a + b) * (a + b)
      = a * a + b * a + (a * b + b * b) : by rw [mul_add, add_mul, add_mul]
  ... = a * a + (b * a + a * b) + b * b : by rw [←add_assoc, add_assoc (a * a)]
  ... = a * a + 2 * (a * b) + b * b     : by rw [mul_comm b a, ←two_mul]

-- 3ª demostración
-- ===============

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

-- 4ª demostración
-- ===============

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

-- 5ª demostración
-- ===============

example :
  (a + b) * (a + b) = a * a + 2 * (a * b) + b * b :=
begin
  rw mul_add,
  rw add_mul,
  rw add_mul,
  rw ← add_assoc,
  rw add_assoc (a * a),
  rw mul_comm b a,
  rw ← two_mul,
end

-- 6ª demostración
-- ===============

example :
  (a + b) * (a + b) = a * a + 2 * (a * b) + b * b :=
begin
  rw [mul_add, add_mul, add_mul],
  rw [←add_assoc, add_assoc (a * a)],
  rw [mul_comm b a, ←two_mul],
end


/-
Copyright (c) 2020 Aaron Anderson. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Aaron Anderson
-/
import data.finset.fold
import algebra.gcd_monoid.multiset

/-!
# GCD and LCM operations on finsets

## Main definitions

- `finset.gcd` - the greatest common denominator of a `finset` of elements of a `gcd_monoid`
- `finset.lcm` - the least common multiple of a `finset` of elements of a `gcd_monoid`

## Implementation notes

Many of the proofs use the lemmas `gcd.def` and `lcm.def`, which relate `finset.gcd`
and `finset.lcm` to `multiset.gcd` and `multiset.lcm`.

TODO: simplify with a tactic and `data.finset.lattice`

## Tags

finset, gcd
-/

variables {α β γ : Type*}

namespace finset
open multiset

variables [cancel_comm_monoid_with_zero α] [normalized_gcd_monoid α]

/-! ### lcm -/
section lcm

/-- Least common multiple of a finite set -/
def lcm (s : finset β) (f : β → α) : α := s.fold gcd_monoid.lcm 1 f

variables {s s₁ s₂ : finset β} {f : β → α}

lemma lcm_def : s.lcm f = (s.1.map f).lcm := rfl

@[simp] lemma lcm_empty : (∅ : finset β).lcm f = 1 :=
fold_empty

@[simp] lemma lcm_dvd_iff {a : α} : s.lcm f ∣ a ↔ (∀b ∈ s, f b ∣ a) :=
begin
  apply iff.trans multiset.lcm_dvd,
  simp only [multiset.mem_map, and_imp, exists_imp_distrib],
  exact ⟨λ k b hb, k _ _ hb rfl, λ k a' b hb h, h ▸ k _ hb⟩,
end

lemma lcm_dvd {a : α} : (∀b ∈ s, f b ∣ a) → s.lcm f ∣ a :=
lcm_dvd_iff.2

lemma dvd_lcm {b : β} (hb : b ∈ s) : f b ∣ s.lcm f :=
lcm_dvd_iff.1 dvd_rfl _ hb

@[simp] lemma lcm_insert [decidable_eq β] {b : β} :
  (insert b s : finset β).lcm f = gcd_monoid.lcm (f b) (s.lcm f) :=
begin
  by_cases h : b ∈ s,
  { rw [insert_eq_of_mem h,
        (lcm_eq_right_iff (f b) (s.lcm f) (multiset.normalize_lcm (s.1.map f))).2 (dvd_lcm h)] },
  apply fold_insert h,
end

@[simp] lemma lcm_singleton {b : β} : ({b} : finset β).lcm f = normalize (f b) :=
multiset.lcm_singleton

@[simp] lemma normalize_lcm : normalize (s.lcm f) = s.lcm f := by simp [lcm_def]

lemma lcm_union [decidable_eq β] : (s₁ ∪ s₂).lcm f = gcd_monoid.lcm (s₁.lcm f) (s₂.lcm f) :=
finset.induction_on s₁ (by rw [empty_union, lcm_empty, lcm_one_left, normalize_lcm]) $ λ a s has ih,
  by rw [insert_union, lcm_insert, lcm_insert, ih, lcm_assoc]

theorem lcm_congr {f g : β → α} (hs : s₁ = s₂) (hfg : ∀a ∈ s₂, f a = g a) :
  s₁.lcm f = s₂.lcm g :=
by { subst hs, exact finset.fold_congr hfg }

lemma lcm_mono_fun {g : β → α} (h : ∀ b ∈ s, f b ∣ g b) : s.lcm f ∣ s.lcm g :=
lcm_dvd (λ b hb, (h b hb).trans (dvd_lcm hb))

lemma lcm_mono (h : s₁ ⊆ s₂) : s₁.lcm f ∣ s₂.lcm f :=
lcm_dvd $ assume b hb, dvd_lcm (h hb)

theorem lcm_eq_zero_iff [nontrivial α] : s.lcm f = 0 ↔ 0 ∈ f '' s :=
by simp only [multiset.mem_map, lcm_def, multiset.lcm_eq_zero_iff, set.mem_image, mem_coe,
  ← finset.mem_def]

end lcm

/-! ### gcd -/
section gcd

/-- Greatest common divisor of a finite set -/
def gcd (s : finset β) (f : β → α) : α := s.fold gcd_monoid.gcd 0 f

variables {s s₁ s₂ : finset β} {f : β → α}

lemma gcd_def : s.gcd f = (s.1.map f).gcd := rfl

@[simp] lemma gcd_empty : (∅ : finset β).gcd f = 0 :=
fold_empty

lemma dvd_gcd_iff {a : α} : a ∣ s.gcd f ↔ ∀b ∈ s, a ∣ f b :=
begin
  apply iff.trans multiset.dvd_gcd,
  simp only [multiset.mem_map, and_imp, exists_imp_distrib],
  exact ⟨λ k b hb, k _ _ hb rfl, λ k a' b hb h, h ▸ k _ hb⟩,
end

lemma gcd_dvd {b : β} (hb : b ∈ s) : s.gcd f ∣ f b :=
dvd_gcd_iff.1 dvd_rfl _ hb

lemma dvd_gcd {a : α} : (∀b ∈ s, a ∣ f b) → a ∣ s.gcd f :=
dvd_gcd_iff.2

@[simp] lemma gcd_insert [decidable_eq β] {b : β} :
  (insert b s : finset β).gcd f = gcd_monoid.gcd (f b) (s.gcd f) :=
begin
  by_cases h : b ∈ s,
  { rw [insert_eq_of_mem h,
    (gcd_eq_right_iff (f b) (s.gcd f) (multiset.normalize_gcd (s.1.map f))).2 (gcd_dvd h)] ,},
  apply fold_insert h,
end

@[simp] lemma gcd_singleton {b : β} : ({b} : finset β).gcd f = normalize (f b) :=
multiset.gcd_singleton

@[simp] lemma normalize_gcd : normalize (s.gcd f) = s.gcd f := by simp [gcd_def]

lemma gcd_union [decidable_eq β] : (s₁ ∪ s₂).gcd f = gcd_monoid.gcd (s₁.gcd f) (s₂.gcd f) :=
finset.induction_on s₁ (by rw [empty_union, gcd_empty, gcd_zero_left, normalize_gcd]) $
  λ a s has ih, by rw [insert_union, gcd_insert, gcd_insert, ih, gcd_assoc]

theorem gcd_congr {f g : β → α} (hs : s₁ = s₂) (hfg : ∀a ∈ s₂, f a = g a) :
  s₁.gcd f = s₂.gcd g :=
by { subst hs, exact finset.fold_congr hfg }

lemma gcd_mono_fun {g : β → α} (h : ∀ b ∈ s, f b ∣ g b) : s.gcd f ∣ s.gcd g :=
dvd_gcd (λ b hb, (gcd_dvd hb).trans (h b hb))

lemma gcd_mono (h : s₁ ⊆ s₂) : s₂.gcd f ∣ s₁.gcd f :=
dvd_gcd $ assume b hb, gcd_dvd (h hb)

theorem gcd_image {g : γ → β} (s: finset γ) [decidable_eq β] [is_idempotent α gcd_monoid.gcd] :
  (s.image g).gcd f = s.gcd (f ∘ g) := by simp [gcd, fold_image_idem]

theorem gcd_eq_gcd_image [decidable_eq α] [is_idempotent α gcd_monoid.gcd] :
  s.gcd f = (s.image f).gcd id := (@gcd_image _ _ _ _ _ id _ _ _ _).symm

theorem gcd_eq_zero_iff : s.gcd f = 0 ↔ ∀ (x : β), x ∈ s → f x = 0 :=
begin
  rw [gcd_def, multiset.gcd_eq_zero_iff],
  split; intro h,
  { intros b bs,
    apply h (f b),
    simp only [multiset.mem_map, mem_def.1 bs],
    use b,
    simp [mem_def.1 bs] },
  { intros a as,
    rw multiset.mem_map at as,
    rcases as with ⟨b, ⟨bs, rfl⟩⟩,
    apply h b (mem_def.1 bs) }
end

lemma gcd_eq_gcd_filter_ne_zero [decidable_pred (λ (x : β), f x = 0)] :
  s.gcd f = (s.filter (λ x, f x ≠ 0)).gcd f :=
begin
  classical,
  transitivity ((s.filter (λ x, f x = 0)) ∪ (s.filter (λ x, f x ≠ 0))).gcd f,
  { rw filter_union_filter_neg_eq },
  rw gcd_union,
  transitivity gcd_monoid.gcd (0 : α) _,
  { refine congr (congr rfl _) rfl,
    apply s.induction_on, { simp },
    intros a s has h,
    rw filter_insert,
    split_ifs with h1; simp [h, h1], },
  simp [gcd_zero_left, normalize_gcd],
end

lemma gcd_mul_left {a : α} : s.gcd (λ x, a * f x) = normalize a * s.gcd f :=
begin
  classical,
  apply s.induction_on,
  { simp },
  intros b t hbt h,
  rw [gcd_insert, gcd_insert, h, ← gcd_mul_left],
  apply ((normalize_associated a).mul_right _).gcd_eq_right
end

lemma gcd_mul_right {a : α} : s.gcd (λ x, f x * a) = s.gcd f * normalize a :=
begin
  classical,
  apply s.induction_on,
  { simp },
  intros b t hbt h,
  rw [gcd_insert, gcd_insert, h, ← gcd_mul_right],
  apply ((normalize_associated a).mul_left _).gcd_eq_right
end

end gcd
end finset

namespace finset
section is_domain

variables [comm_ring α] [is_domain α] [normalized_gcd_monoid α]

lemma gcd_eq_of_dvd_sub {s : finset β} {f g : β → α} {a : α}
  (h : ∀ x : β, x ∈ s → a ∣ f x - g x) :
  gcd_monoid.gcd a (s.gcd f) = gcd_monoid.gcd a (s.gcd g) :=
begin
  classical,
  revert h,
  apply s.induction_on,
  { simp },
  intros b s bs hi h,
  rw [gcd_insert, gcd_insert, gcd_comm (f b), ← gcd_assoc, hi (λ x hx, h _ (mem_insert_of_mem hx)),
      gcd_comm a, gcd_assoc, gcd_comm a (gcd_monoid.gcd _ _),
      gcd_comm (g b), gcd_assoc _ _ a, gcd_comm _ a],
  exact congr_arg _ (gcd_eq_of_dvd_sub_right (h _ (mem_insert_self _ _)))
end

end is_domain

end finset


/-
In this file, we'll start to build a certified abstract
data type for the natural numbers. What we want is for
our implementations to faithfully present the properties
of natural numbers that we're familar with from highschool
algebra. For example, zero is a left identify for addition;
zero is also a right identity; addition is associative and
commutative; the distributive law holds; etc. We'll get
you started, and you will then finish off development of
a certified abstract data type for natural numbers.
-/

namespace hidden

/-
DATA TYPE
-/
inductive nt
| zero
| succ (n' : nt)

open nt

/-
Some example values of this type, with nice names.
-/
def one := succ zero
def two := succ (succ zero)
def three := succ (succ (succ zero))
def four := succ three  -- works just fine
def five := succ four
def six := succ five

-- tests
#reduce four
#reduce five
#reduce six

/-
OPERATIONS
-/

-- COMPUTATIONAL

/-
This implementation of the identity function can
be read as saying that id is a function from nt
to nt, and in particular when applied to any
argument, n, it returns n. Importantly, we don't
need to "analyze/destructure" n to decide what
to return: we just return whatever we got.
-/
def id : nt → nt
| n := n

-- we can compute results of evaluating applications
#reduce (id two)

/-
We can also write test cases as equality propositions
that assert that actual outputs are equal to expected
outputs, with simple equality proofs. The failure of
rfl here indicates that either the computation or the
test case is wrong.
-/
example : id two = two := rfl
example : id three = two := rfl -- oops, bad test case

/-
A similar approaches works for defining increment.
-/
def inc : nt → nt
| n := succ n

/-
Next we'll look at the decrement function, defined
mathemtically as mapping 0 to 0 and and any positive
natural number, n = n'+1, to n', i.e., to n-1. Make
sure you understand what we just said! To implement
this function, we need to *analyze/destructure* the
argument in order to determine if it's zero or some
non-zero number (which is to say, the successor of
some one-smaller natural number, n').
-/
def dec : nt → nt
| zero := zero
| (succ n') := n'   -- you must understand this!

-- tests
#reduce dec two     -- expect one
#reduce dec one     -- expect zero
#reduce dec zero    -- expect zero

/-
Test cases as equality propositions for
individual inputs.
-/

example : dec two = one := rfl
example : dec one = zero := rfl
example : dec zero = zero := rfl

/-
The key to this definition is in the pattern match
that occurs in the second case. Take, for example,
the expression (dec two). To evaluate this we first
evaluate the identifier expression, two, which then
unfolds to (succ (succ zero)), then we do pattern
matching. This term does not match the term, zero,
so Lean moves on to match it with (succ n').

The essential technical concept at this point what
we call unification. Lean sees that the pattern,
(succ n'), can be unified with (succ (succ zero)),
where the "succ" in (succ n') matches the first
"succ" in (succ (succ zero)), and where n' matches
the rest, namely (succ zero); and that n' is what
gets returned.

The big idea is that you can use pattern matching
to "analyze" a term (argument in this case), to pull
it apart into subterms pieces, giving subterms names
(here n') that we can then use to express the return
result to the right of the colon-equals separator.
-/

/-
Next, we define an isZero "predicate function", a
function that returns true if an argument has a
particular property (here that of being zero), or
false if it doesn't. We again have to analyze the
argument (doing a kind of case analysis). The one
new concept introduced here is that sometimes you
will want to match on any value of a given argument
without giving it a name. This function matches on
its argument to determine if it's zero, in which
case the function returns true, otherwise it just
returns false without further naming or analysis
of the argument value.
-/
def isZero : nt → bool
| zero := tt
| _ := ff

/-
Another example of a function where there's no
need to analyze the argument: this function takes
a natural number and returns zero no matter what
it is.
-/
def const_zero : nt → nt
| _ := zero

/-
As an aside, it would be preferable for readability
to use simpler syntax to define this function. Here
is an alternative.
-/

def const_zero' (n : nt) := zero

-- some simple tests
#check const_zero'
#reduce const_zero' six -- expect zero

-- binary operations

/-
Addition is defined as iterated application of the
increment (inc) function to the second argument the
first argument number of times. For example, 3 + 2
reduces to 1 + 1 + 1 + 2. That's three applications
of inc to two.
-/
def add : nt → nt → nt
| zero m := m
| (succ n') m := succ (add n' m)

-- tests
#reduce add two three                 -- check by eye
example : add two three = five := rfl -- prove it

/-
Multiplication implemented as iteration of
addition of the second argument the first
argument number of times. Note that iterating
multiplication 0 times is defined to be one,
while iterating addition zero times is defined
to be zero.
-/
def mul : nt → nt → nt
| zero m := zero
| (succ n') m := add (mul n' m) m

-- test
example : mul two three = six := rfl

/-
Problem: represent the square function
on natural numbers declaratively.
-/

/-
Exponentiation is defined to be iteration
of multiplication of the first argument the
second argument number of times. Take some
time to really study the similarities and
differences in the definitions of add, mul,
and exp.
-/
def exp : nt → nt → nt
| n zero := one
| n (succ m') := mul n (exp n m')

-- tests
example : exp two three = succ (succ six) := rfl
example : exp zero three = zero := rfl
example : exp three zero = one := rfl

/-
PROOFS OF PROPERTIES

There are many properties we can try to prove. As
a starter, let's try to prove that zero is a left
identity for addition.

def add : nt → nt → nt
| zero m := m
| (succ n') m := succ (add n' m)
-/
example : ∀ (m : nt), add zero m = m := by simp [add]

/-
The crucial point in this case is that we already
know that, ∀ (m : nt), add zero m = m, *from the
definition of add*. In other words, add zero m is
*definitionally* equal to m.

  def add : nt → nt → nt
  | zero m := m
  | (succ n') m := succ (add n' m)

The first rule serves as an axiom that allows us
instantly to conclude that: ∀ m, add zero m = m.
The proof is by simply invoking this axiom, which
we do by using the simplify (simp) tactic in Lean,
pointing it to the definition whose rules we want
it to employ.

Note that "by" is a way of introducing a proof
written using tactics without having the write
a complete "begin ... end" block.
-/

/-
Perhaps somewhat surprisingly, we hit a roadblock when
we try to prove that zero is a *right* identity. We don't
have an axiom for that! Rather we'll need to prove it
as a theorem.
-/
example : ∀ (m : nt), add m zero = m := by simp [add] --fail
/-
def add : nt → nt → nt
| zero m := m
| (succ n') m := succ (add n' m)
-/

-- Prove it!
theorem zero_is_right_zero : ∀ (m : nt), add m zero = m :=
begin
  assume m,
  induction m with m' h,  -- by induction!
  simp [add],
  simp [add],
  exact h,
end

-- NOTATION

/-
A complete, beautiful, and highly usable "module"
that implements an algebraic structure, such as
Boolean algebra or Peano arithmetic, often needs
to introduce convenient *notations* for applying
operations to arguments. We'd rather write (2 + 3)
than (nat.add zero.succ.succ zero.succ.succ.succ),
for example, even though we understand that they
mean the same thing.

In Lean, we can overload operators, such as +,
that are already defined in Lean's libraries,
and we will thereby inherit both precedence and
associativity properties that were carefully
crafted by the library designers.
-/

notation x + y := add x y
notation x * y := mul x y

#reduce five + six        -- expect 11 .succs of zero
#reduce five * six + two  -- expect 32 .succs of zero

-- Now we can use these notations in writing expressions
example : five + six = zero.succ.succ.succ.succ.succ.succ.succ.succ.succ.succ.succ := rfl

-- HOMEWORK
theorem add_commutes : ∀ (m n : nt), m + n = n + m :=
begin
  assume m n,
  induction m with m' h,
  -- base case
  -- exact rfl,   does NOT work
  simp [add],
  rw zero_is_right_zero,
  -- inductive case
  simp [add],
  rw h,

  --
  induction n with n' k,
  --base case
  rw <-h,
  simp [add],
  rw zero_is_right_zero,
  --inductive case

end

end hidden


import data.real.basic
import chris_hughes_various.exponential.exponential

definition is_deriv (f : ℝ → ℝ) (x : ℝ) (d : ℝ) : Prop :=
-- d = derivative of f at x
-- limit of f(x+h)-f(x) / h = d as h tends to zero
∀ ε > 0, ∃ δ > 0, ∀ h, abs h < δ ∧ h ≠ 0 → abs ((f (x + h) - f x) / h - d) < ε
--#print group

definition is_differentiable_at (f : ℝ → ℝ) (x : ℝ) : Prop :=
∃ d, is_deriv f x d

definition is_differentiable_on (f : ℝ → ℝ) (X : set ℝ) : Prop :=
∀ x ∈ X, is_differentiable_at f x

definition is_differentiable (f : ℝ → ℝ) : Prop :=
∀ x, is_differentiable_at f x

theorem deriv_is_unique (f : ℝ → ℝ) (x : ℝ) (d e : ℝ) :
is_deriv f x d → is_deriv f x e → d = e := sorry

theorem deriv_is_linear1 (f : ℝ → ℝ) (g : ℝ → ℝ) (x : ℝ) (d e : ℝ) :
is_deriv f x d → is_deriv g x e → is_deriv (λ t, f t + g t) x (d + e) := sorry

theorem deriv_is_linear2 (f : ℝ → ℝ) (x : ℝ) (d : ℝ) (μ : ℝ) :
is_deriv f x d → is_deriv (λ t, μ * f t) x (μ * d) := sorry

-- sorry Patrick
theorem product_rule (f g : ℝ → ℝ) (x : ℝ) (d e : ℝ) :
is_deriv f x d → is_deriv g x e → is_deriv (λ t, f t * g t) x (d * g x + f x * e) := sorry

theorem chain_rule (f g : ℝ → ℝ) (x : ℝ) (d e : ℝ) :
is_deriv f x d → is_deriv g (f x) e → is_deriv (g ∘ f) x (e * d) := sorry

theorem exp_deriv (x : ℝ) : is_deriv (λ t : ℝ, (exp t).re) x (exp x).re := sorry