/- 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