/-
First we define a namespace for this lecture. In Lean, a namespace is
simply a way to group a set of definitions with a common prefix. Any
definition `foo` made inside namespace `N` will have its name prefixed
with `N.`, i.e., `N.foo`.
This comment is written as a "block comment".
-/
namespace intro
-- The `inductive` keyword is the command for defining types in Lean. It is used like
-- `class`, `struct`, `data`, and `type` are in other common programming languages.
-- The choice of inductive may seem strange but will become clearer as we move
-- through the course.
--
-- This comment is written with the single line comment of Lean `--`.
/--
`bool` defines the standard boolean type with two values `tt` and `ff`. We
don't use the names `true` and `false` because they are already used for the
logical `true` and logical `false`. Later in the course we will discuss this
distinction in more detail.
In other languages, this definition might look something like
`enum bool { TT, FF }`.
This is an example of a doc string, you can hover over `bool` anywhere in
the editor (after this definition) to see its doc string.
-/
inductive bool : Type
| tt : bool
| ff : bool
-- We can also define namespaced definitions using a compound name as we do
-- below with `bool.not`.
--
-- Note: Its full name will be `intro.bool.nat` since it is defined in an outer
-- namespace.
--
-- The below definition is given by *pattern matching*. Pattern matching is
-- a more powerful version of the switch statement found in mainstream
-- programming languages.
--
-- Pattern matching is an essential part of many functional programming
-- languages and will likely be used in every program we write in Lean.
--
-- Pattern matching allows you to consider the different *constructor*s of an
-- inductive type. In the below example we define `bool.not` by cases.
--
-- If you want to learn about this topic more in depth please see:
-- https://leanprover.github.io/theorem_proving_in_lean/
-- specifically Section 7 & 8 which discuss inductive types and pattern matching.
/-- `bool.not` negates a boolean. -/
def bool.not (b : bool) : bool :=
match b with
| bool.tt := bool.ff
| bool.ff := bool.tt
end
/-- `bool.and` often written as `&&` -/
def bool.and (b1 : bool) (b2 : bool) : bool :=
match b1 with
| bool.tt := b2
| bool.ff := bool.ff
end
-- Lean is different from most languages, since it allows you to not only
-- define *types*, and *functions*, but also proofs.
--
-- These proofs can be written inline and are no different from other
-- definitions in the language, we will talk more in depth about this later.
--
-- For example we can prove that *for all* values of `b1` and `b2`, `bool.and`
-- is commutative.
--
-- Note: This is a property of the program that can be checked _without_
-- executing any code.
--
-- The ablity to write proofs about your programs is what makes Lean unique, and
-- sometimes referred to as a theorem prover.
lemma bool.and_comm :
∀ (b1 b2 : bool),
bool.and b1 b2 = bool.and b2 b1 :=
begin
/-
intros,
cases b1,
{cases b2; simp [bool.and]},
{ cases b2,
{ simp [bool.and] },
{ simp [bool.and] } }
-/
intros; cases b1; cases b2; simp [bool.and]
end
-- We could have also defined `bool.and` equivalently as:
def bool.and' : bool → bool → bool :=
fun b1, fun b2,
match b1 with
| bool.tt := b2
| bool.ff := bool.ff
end
-- We can check the type of a definition using the `#check` command.
#check bool.and
#check bool.and'
-- Observe that both types are the same, this due to the fact that
-- functions in Lean are *curried*. A curried function is a function
-- that takes a single argument at a time and returns another function of
-- arity one. For multiple arguments this means you can apply a single
-- argument to get back a new function, see below for examples.
#check (bool.and bool.tt)
#check (bool.and' bool.tt)
-- As detailed below, there is also sugar to combine currying and pattern matching:
/-- `boo.or` often written as `||` -/
def boo.or (b1 : bool) : bool → bool
| bool.tt := bool.ff
| bool.ff := b1
-- We will discuss Lean's builtin numeric types later on, but for now we
-- will present one possible definition of natural numbers.
/--
A natural number is either `0` or `(1 + n)` where `n` is some natural
number.
We represent this using an inductive type with two constructors, one has
`0` arguments like we saw with `bool`, while `succ` has a different type
`nat → nat`.
The `succ` constructor acts as `(1 + n)`, for example `2` can be
represented as `nat.succ (nat.succ nat.zero)`.
-/
inductive nat : Type
| zero : nat
| succ : nat → nat
-- We demonstrated pattern matching with the *match expression*
-- (`match x with ... end`)
-- earlier, but there is another more compact way to define a function
-- by cases.
--
-- Similar to the way we define `inductive` types we can just define a function by
-- cases, we just need the `def` keyword, a name, and a function type (`A -> R`).
/-- Test whether a natural number is zero. -/
def is_zero : nat → bool
| nat.zero := bool.tt
| (nat.succ _) := bool.ff
-- As we demonstrated earlier we can define a proof about our program inline.
-- This lemma states that is_zero should always return true for `nat.zero`.
lemma is_zero_zero :
is_zero nat.zero = bool.tt :=
by simp [is_zero]
/--
Addition on naturals `ℕ`. We can define addition by simply peeling off every
plus one (getting `n - 1`) and recursively computing the addition of
`(n - 1) + m`.
In the zero case we return `m` resulting in a chain of additions
by one `1 + 1 + ... + m`, that is, `nat.succ (nat.succ ... m)`.
-/
def add (m : nat) : nat → nat
| nat.zero := m
| (nat.succ n) := nat.succ (add n)
def add'' : nat -> nat -> nat
| m nat.zero := m
| m (nat.succ n) := nat.succ (add'' m n)
/- Addition by zero on the right is identity -/
lemma add_zero_right_id :
∀ n, add n nat.zero = n :=
begin
intros,
reflexivity,
-- simp [add]
end
-- Let's try the to prove the other side.
/- Addition by zero on the left is identity -/
lemma add_zero_left_id_fail :
∀ n, add nat.zero n = n :=
begin
intros,
induction n,
{ reflexivity },
{ simp [add], rw ih_1 }
/-
intros,
cases n,
{ reflexivity },
{ cases a,
{ reflexivity },
{ @tactic.fail unit _ _ "This could take a while ..." }
}
-/
end
-- We need some other way to show that this property of addition holds.
-- If we remember back to our discrete math, or proof courses, our old
-- friend induction can come to the rescue.
lemma add_zero_left_id :
forall (n : nat),
add nat.zero n = n :=
begin
intro n,
induction n,
case nat.zero { reflexivity },
case nat.succ {
/-
Find the left hand side of IHn in the goal, and
replace it with the right hand side of IHn.
-/
dsimp [add],
rw ih_1,
}
end
/-- Here is another reasonable definition for `add`.
Note how it differs from the definition above. -/
def add' : nat → nat → nat
| nat.zero n2 := n2
| (nat.succ m1) n2 := add' m1 (nat.succ n2)
/-- EXERCISE: complete the following proof, see the
solutions in `solutions` namespace at bottom of file.
lemma add_succ_succ_add' :
forall x y,
add x (nat.succ y) = nat.succ (add x y) :=
begin
intros,
simp [add]
end
-/
lemma add_succ_succ_add :
forall y x,
add (nat.succ x) y = nat.succ (add x y) :=
begin
intro,
induction y; intros,
{ simp [add, add'] },
{ simp [add, add'], rw ih_1 }
end
-- EXERCISE: Complete the following proof.
-- Hint: at some point in the proof, rewrite by `add_succ_succ_add`.
lemma add_add' :
forall x y,
add x y = add' x y :=
begin
intro,
induction x; intro,
case nat.zero {
simp [add, add'],
rw add_zero_left_id,
},
case nat.succ {
simp [add, add'],
rw add_succ_succ_add,
rename y foo,
rw <- ih_1,
simp [add],
}
end
-- We can also define subtraction by one `n - 1` on natural numbers.
/-- Subtraction by one. -/
def pred_fail : nat → nat
| (nat.succ m) := m
| nat.zero := sorry -- intro sorry
-- We can now define a *parametric* inductive type, this is an inductive type
-- which is *generic*.
--
-- This is similar to generics in other programming languages, and allows to
-- define a generic container type.
--
-- `option t` is a type which allows us to encode nullability, either a value of
-- `option t` is `none` (the null value), or `some v` where `v` is a value of
-- type `t`.
inductive option (t : Type)
| none {} : option -- the `{}` annotation tells Lean to infer the type argument `t` to option
| some : t → option
-- Parameters to inductive types are automatically inferred by Lean, in order to
-- do this Lean needs some amount of context in order to figure out what type
-- `none` has.
def option_int : option int := option.none
#check option_int
-- If we check it without context we can see that Lean leaves a guess for the type `?M` in place
-- of the type argument.
#check (none)
-- In the `some` case, the value `nat.zero` allows Lean to solve `t = nat`.
#check (some nat.zero)
/-- Subtraction by one. -/
def pred : nat → option nat
| (nat.succ m) := option.some m
| (nat.zero) := option.none
#eval (pred nat.zero)
#eval (pred (nat.succ nat.zero))
#eval (pred (nat.succ (nat.succ nat.zero)))
#eval (pred (nat.succ (nat.succ (nat.succ nat.zero))))
--
-- In practice, we often us [option] in places
-- where you might throw an exception in other
-- programming languages.
--
-- How many values of type [option] are there?
-- Well, it depends on what option is being
-- paramaterized over. [option bool] has 3
-- values:
-- <<
-- None
-- Some true
-- Some false
-- >>
-- [option nat] on the other hand has infinitely
-- many values:
-- <<
-- None
-- Some O
-- Some (S 0)
-- Some (S (S O))
-- Some (S (S (S O)))
-- ...
-- >>
--
-- We can easily prove properties about our code that we would normally try to
-- establish using unit tests, but instead we can check them for *all* executions
-- without needing to run our program.
lemma pred_none :
forall n,
pred n = option.none ->
n = nat.zero :=
begin
intros,
cases n,
{ reflexivity },
{ simp [pred] at a,
contradiction }
end
-- `option` only lets us indicate 0 or 1 results
-- from a function. As we'll study more next lecture,
-- we can encode an arbitrary number of results
-- using `list`.
inductive list (t : Type) : Type
| nil {} : list
| cons : t → list → list
def length {t : Type} : list t → nat
| list.nil := nat.zero
| (list.cons x xs) := nat.succ (length xs)
/- Note: The curly braces `{t : Type}` simply indicate
that Lean should try to infer the `t` argument.
In this case, Lean will never have any trouble
figuring what `t` should be because the type of
the `l` will always unambiguously indicate what
`t` ought to be. -/
-- Solutions
namespace solutions
lemma add_succ_succ_add :
forall y x,
add (nat.succ x) y = nat.succ (add x y) :=
begin
intro,
induction y; intros,
{ simp [add, add'] },
{ simp [add, add'], rw ih_1 }
end
lemma add_add' :
forall x y,
add x y = add' x y :=
begin
intro,
induction x; intros,
{ simp [add, add'],
rw add_zero_left_id
},
{ simp [add, add'],
rw add_succ_succ_add,
rw ← ih_1,
simp [add],
}
end
end solutions
end intro