https://emallson.net/blog/a-beginners-companion-to-theorem-proving-in-lean/ Home Portfolio Blog A Beginner's Companion to Theorem Proving in Lean 4 Written by J. David Smith Published on 27 December 2023 This year, one of my hobby projects has been implementing some basic properties and conversions between Matroids in Lean. My primary resource for doing this has been Theorem Proving in Lean 4 (TPiL), which is an incredibly detailed walk through using Lean as a proof assistant. While useful, it has...gaps--as does the main Lean documentation. A lot of this gap appears to be filled by the Zulip chat. The title of this post is a bit of a double entendre: I am still a relative beginner, writing what I intend as a companion to TPiL; and this is is intended for beginners to Lean, covering that I wish I had known as a beginner myself. A final caveat emptor: I haven't really hung out in Zulip and don't have a good grasp on good code organization and style in Lean. These may not be best practices, but they helped me. --------------------------------------------------------------------- This is organized in small sections covering individual things that I learned the hard way while working on my hobby project. Running Snippets You can run the code snippets below in Lean or on the Lean website if you paste this prelude at the top: import Mathlib.Data.Finset.Basic import Mathlib.Data.Finset.Card Theorem Location with rw?, simp?, apply? and exact? example (S T : Finset a) : S [?] T -> T [?] S -> T = S := by exact? One of the biggest pain points early in my Lean experience was finding theorems to use. The mathlib documentation is great, but lacking hoogle-style search. EDIT: Apparently not! LeahNeukirchen points out on lobste.rs two different hoogle-like search engines that I missed: * moogle.ai * loogle The rw?, simp?, apply? and exact? tactics give you specialized hoogle-like access within the Lean environment, finding candidate theorems that satisfy a specific goal and listing them in the editor UI. You can pick one of the solutions via code actions if using the LSP. By default, all target the current goal, but rw? and simp? can have at forms: example (S T : Finset a) : S [?] T -> T [?] S -> T = S := by intro S_subs T_subs rw? at S_subs Goal Patterns for rw, apply and exact TPiL explains what each of these tactics do in technical terms, but it took time for me to grok what each do in practice. It is important to understand that these tactics are all fundamentally performing pattern rewriting. The patterns for each are: rw Input Goal: a Assumption: a = b or a - b Output Goal: b rw is the most direct of the 3. It applies a theorem or assumption that looks like a = b or a <-> b to a goal that looks like a, substituting it with b. rw, unlike apply and exact, can be applied to non-goals to transform them. This is very useful to massage theorems from different sources into alignment to prove your goal. example (S T X : Finset a) : S = X -> X = T -> S = T := by intro S_eq X_eq rw [S_eq, <-X_eq] exact Input Goal: a Assumption: a Output Goal: No Goal exact takes an assumption, along with any needed parameters, and uses it to resolve the current goal. It is simplistic, but effective. example (S T : Finset a) : S [?] T -> T [?] S -> S = T := by intro S_subs T_subs /- Finset.Subset.antisymm looks like: S [?] T -> T [?] S -> S = T -/ /- the S_subs and T_subs parameters leave us with S = T, which matches the goal. -/ exact Finset.Subset.antisymm S_subs T_subs apply Input Goal: b Assumption: a - b Output Goal: a apply transforms your goal into a different goal using an implication. If a proof of a exists in the current scope, it is applied (in which case you basically get exact). Otherwise, you get a new goal a. example (S T : Finset a) : S [?] T [?] T [?] S := by /- Finset.Subset.antisymm_iff.mp looks like S = T -> S [?] T [?] T [?] S -/ apply Finset.Subset.antisymm_iff.mp /- new goal: S = T -/ Importantly (so importantly I'll bring it up again later): an a - b or a = b assumption can be converted to a - b (with .mp) or b - a (with .mpr). Introducing New Sub-Goals with have and if/else TPiL mentions sub-goals at several points and does briefly discuss have, but I feel this area was glossed over. Adding sub-goals toward your larger goal is an extremely helpful method of making larger proofs tractable. The first (and seemingly most common) way to introduce sub-goals is by way of have, which generally has two purposes. First: you can introduce smaller goals that are easier to prove--sometimes even possible for Lean to prove automatically. /- While lean cannot auto-solve the outer statement, it can trivially solve the both steps with `exact?` -/ example [DecidableEq a] (S : Finset a) (e1 e2 : a) : e2 [?] S - insert e2 (insert e1 S) = (insert e1 S) := by intro e2_mem have : e2 [?] (insert e1 S) := by exact? exact? Second: you can use it to instantiate other theorems for re-use: /- (not runnable) -/ have T_card := ind_rank_eq_largest_ind_subset_card ind (insert e2 S) T T_subs T_ind T_max In contrast, if / else is generally useful in cases where you need to introduce a goal that relies on the law of the excluded middle. /- there is a theorem in Mathlib for this, but we use `have` to create smaller goals to bypass it -/ example [DecidableEq a] (S : Finset a) (e : a) : (insert e S).card <= S.card + 1 := by if h : e [?] S then have : insert e S = S := Finset.insert_eq_self.mpr h rw [this] exact Nat.le_add_right (Finset.card S) 1 else have : S.card + 1 = (insert e S).card := (Finset.card_insert_of_not_mem h).symm rw [this] Note that this, along with helpful theorems like not_not rely on classical logic and as such are not strictly constructive (as far as my knowledge indicates), but if/else in particular is such a useful means of organizing my thoughts in the implementation of a proof that I opted to use it quite heavily. EDIT: It appears that Lean will only allow non-constructive uses of if / else if you have used open Classical in the module. More info refine and case TLiP discusses handling cases with dots and cases _ with and match _ with, but it wasn't clear to me how to handle the (labelled) sub-goals generated other tools (like apply Iff.intro). You can use case