Theorem Proving in Lean 4

6. Interacting with Lean

You are now familiar with the fundamentals of dependent type theory, both as a language for defining mathematical objects and a language for constructing proofs. The one thing you are missing is a mechanism for defining new data types. We will fill this gap in the next chapter, which introduces the notion of an inductive data type. But first, in this chapter, we take a break from the mechanics of type theory to explore some pragmatic aspects of interacting with Lean.

Not all of the information found here will be useful to you right away. We recommend skimming this section to get a sense of Lean's features, and then returning to it as necessary.

6.1. Messages🔗

Lean produces three kinds of messages:

Errors

Errors are produced when an inconsistency in the code means that it can't be processed. Examples include syntax errors (e.g. a missing )) and type errors such as attempting to add a natural number to a function.

Warnings

Warnings describe potential problems with the code, such as the presence of sorry. Unlike with errors, the code is not meaningless; however, warnings deserve careful attention.

Information

Information doesn't indicate any problem with the code, and includes output from commands such as #check and #eval.

Lean can check that a command produces the expected messages. If the messages match, then any errors are disregarded; this can be used to ensure that the right errors occur. If they don't, an error is produced. You can use the #guard_msgs command to indicate which messages are expected.

Here is an example:

/-- error: type mismatch "Not a number" has type String : Type but is expected to have type Nat : Type -/ #guard_msgs in def x : Nat := "Not a number"

Including a message category in parentheses after #guard_msgs causes it to check only the specified category, letting others through. In this example, #eval issues an error due to the presence of sorry, but the warning that is always issued for sorry is displayed as usual:

/-- error: aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes. To attempt to evaluate anyway despite the risks, use the '#eval!' command. -/ #guard_msgs(error) in declaration uses 'sorry'#eval (sorry : Nat)
declaration uses 'sorry'

Without the configuration, both messages are captured:

/-- error: aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes. To attempt to evaluate anyway despite the risks, use the '#eval!' command. --- warning: declaration uses 'sorry' -/ ❌️ Docstring on `#guard_msgs` does not match generated message: warning: declaration uses 'sorry' --- error: aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes. To attempt to evaluate anyway despite the risks, use the '#eval!' command.#guard_msgs in declaration uses 'sorry'aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes. To attempt to evaluate anyway despite the risks, use the '#eval!' command.#eval (sorry : Nat)
❌️ Docstring on `#guard_msgs` does not match generated message:

warning: declaration uses 'sorry'
---
error: aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.

Some examples in this book use #guard_msgs to indicate expected errors.

6.2. Importing Files🔗

The goal of Lean's front end is to interpret user input, construct formal expressions, and check that they are well-formed and type-correct. Lean also supports the use of various editors, which provide continuous checking and feedback. More information can be found on the Lean documentation pages.

The definitions and theorems in Lean's standard library are spread across multiple files. Users may also wish to make use of additional libraries, or develop their own projects across multiple files. When Lean starts, it automatically imports the contents of the library Init folder, which includes a number of fundamental definitions and constructions. As a result, most of the examples we present here work “out of the box.”

If you want to use additional files, however, they need to be imported manually, via an import statement at the beginning of a file. The command

import Bar.Baz.Blah

imports the file Bar/Baz/Blah.olean, where the descriptions are interpreted relative to the Lean search path. Information as to how the search path is determined can be found on the documentation pages. By default, it includes the standard library directory, and (in some contexts) the root of the user's local project.

Importing is transitive. In other words, if you import Foo and Foo imports Bar, then you also have access to the contents of Bar, and do not need to import it explicitly.

6.3. More on Sections🔗

Lean provides various sectioning mechanisms to help structure a theory. You saw in Variables and Sections that the section command makes it possible not only to group together elements of a theory that go together, but also to declare variables that are inserted as arguments to theorems and definitions, as necessary. Remember that the point of the variable command is to declare variables for use in theorems, as in the following example:

section variable (x y : Nat) def double := x + x double y : Nat#check double y
double y : Nat
double (2 * x) : Nat#check double (2 * x)
double (2 * x) : Nat
attribute [local simp] Nat.add_assoc Nat.add_comm Nat.add_left_comm theorem t1 : double (x + y) = double x + double y := x:Naty:Natdouble (x + y) = double x + double y All goals completed! 🐙 t1 y : ∀ (y_1 : Nat), double (y + y_1) = double y + double y_1#check t1 y
t1 y : ∀ (y_1 : Nat), double (y + y_1) = double y + double y_1
t1 (2 * x) : ∀ (y : Nat), double (2 * x + y) = double (2 * x) + double y#check t1 (2 * x)
t1 (2 * x) : ∀ (y : Nat), double (2 * x + y) = double (2 * x) + double y
theorem t2 : double (x * y) = double x * y := x:Naty:Natdouble (x * y) = double x * y All goals completed! 🐙 end

The definition of double does not have to declare x as an argument; Lean detects the dependence and inserts it automatically. Similarly, Lean detects the occurrence of x in t1 and t2, and inserts it automatically there, too. Note that double does not have y as argument. Variables are only included in declarations where they are actually used.

6.4. More on Namespaces🔗

In Lean, identifiers are given by hierarchical names like Foo.Bar.baz. We saw in Namespaces that Lean provides mechanisms for working with hierarchical names. The command namespace Foo causes Foo to be prepended to the name of each definition and theorem until end Foo is encountered. The command open Foo then creates temporary aliases to definitions and theorems that begin with prefix Foo.

namespace Foo def bar : Nat := 1 end Foo open Foo Foo.bar : Nat#check bar
Foo.bar : Nat
Foo.bar : Nat#check Foo.bar
Foo.bar : Nat

The following definition

def Foo.bar : Nat := 1

is treated as a macro, and expands to

namespace Foo def bar : Nat := 1 end Foo

Although the names of theorems and definitions have to be unique, the aliases that identify them do not. When we open a namespace, an identifier may be ambiguous. Lean tries to use type information to disambiguate the meaning in context, but you can always disambiguate by giving the full name. To that end, the string _root_ is an explicit description of the empty prefix.

def String.add (a b : String) : String := a ++ b def Bool.add (a b : Bool) : Bool := a != b def add (α β : Type) : Type := Sum α β open Bool open String -- This reference is ambiguous: -- #check add String.add (a b : String) : String#check String.add
String.add (a b : String) : String
Bool.add (a b : Bool) : Bool#check Bool.add
Bool.add (a b : Bool) : Bool
add (α β : Type) : Type#check _root_.add
add (α β : Type) : Type
"hello".add "world" : String#check add "hello" "world"
"hello".add "world" : String
true.add false : Bool#check add true false
true.add false : Bool
_root_.add Nat Nat : Type#check add Nat Nat
_root_.add Nat Nat : Type

We can prevent the shorter alias from being created by using the protected keyword:

protected def Foo.bar : Nat := 1 open Foo /-- error: unknown identifier 'bar' -/ #guard_msgs in #check bar -- error Foo.bar : Nat#check Foo.bar
Foo.bar : Nat

This is often used for names like Nat.rec and Nat.recOn, to prevent overloading of common names.

The open command admits variations. The command

open Nat (succ zero gcd) Nat.zero : Nat#check zero
Nat.zero : Nat
3#eval gcd 15 6
3

creates aliases for only the identifiers listed. The command

open Nat hiding succ gcd Nat.zero : Nat#check zero
Nat.zero : Nat
/-- error: unknown identifier 'gcd' -/ #guard_msgs in #eval gcd 15 6 -- error 3#eval Nat.gcd 15 6
3

creates aliases for everything in the Nat namespace except the identifiers listed.

open Nat renaming mul times, add plus 7#eval plus (times 2 2) 3
7

creates aliases renaming Nat.mul to times and Nat.add to plus.

It is sometimes useful to export aliases from one namespace to another, or to the top level. The command

export Nat (succ add sub)

creates aliases for succ, add, and sub in the current namespace, so that whenever the namespace is open, these aliases are available. If this command is used outside a namespace, the aliases are exported to the top level.

6.5. Attributes🔗

The main function of Lean is to translate user input to formal expressions that are checked by the kernel for correctness and then stored in the environment for later use. But some commands have other effects on the environment, either assigning attributes to objects in the environment, defining notation, or declaring instances of type classes, as described in the chapter on type classes. Most of these commands have global effects, which is to say, they remain in effect not only in the current file, but also in any file that imports it. However, such commands often support the local modifier, which indicates that they only have effect until the current section or namespace is closed, or until the end of the current file.

In Using the Simplifier, we saw that theorems can be annotated with the [simp] attribute, which makes them available for use by the simplifier. The following example defines the prefix relation on lists, proves that this relation is reflexive, and assigns the [simp] attribute to that theorem.

def isPrefix (l₁ : List α) (l₂ : List α) : Prop := t, l₁ ++ t = l₂ @[simp] theorem List.isPrefix_self (as : List α) : isPrefix as as := [], α:Type u_1as:List αas ++ [] = as All goals completed! 🐙 example : isPrefix [1, 2, 3] [1, 2, 3] := isPrefix [1, 2, 3] [1, 2, 3] All goals completed! 🐙

The simplifier then proves isPrefix [1, 2, 3] [1, 2, 3] by rewriting it to True.

One can also assign the attribute any time after the definition takes place:

theorem List.isPrefix_self (as : List α) : isPrefix as as := [], α:Type u_1as:List αas ++ [] = as All goals completed! 🐙 attribute [simp] List.isPrefix_self

In all these cases, the attribute remains in effect in any file that imports the one in which the declaration occurs. Adding the local modifier restricts the scope:

section theorem List.isPrefix_self (as : List α) : isPrefix as as := [], α:Type u_1as:List αas ++ [] = as All goals completed! 🐙 attribute [local simp] List.isPrefix_self example : isPrefix [1, 2, 3] [1, 2, 3] := isPrefix [1, 2, 3] [1, 2, 3] All goals completed! 🐙 end /-- error: simp made no progress -/ #guard_msgs in example : isPrefix [1, 2, 3] [1, 2, 3] := isPrefix [1, 2, 3] [1, 2, 3] isPrefix [1, 2, 3] [1, 2, 3]

For another example, we can use the instance command to assign the notation to the isPrefix relation. That command, which will be explained in the chapter on type classes, works by assigning an [instance] attribute to the associated definition.

def isPrefix (l₁ : List α) (l₂ : List α) : Prop := t, l₁ ++ t = l₂ instance : LE (List α) where le := isPrefix theorem List.isPrefix_self (as : List α) : as as := [], α:Type u_1as:List αas ++ [] = as All goals completed! 🐙

That assignment can also be made local:

def instLe : LE (List α) := { le := isPrefix } section attribute [local instance] instLe example (as : List α) : as as := [], α:Type u_1as:List αas ++ [] = as All goals completed! 🐙 end /-- error: failed to synthesize LE (List α) Additional diagnostic information may be available using the `set_option diagnostics true` command. -/ #guard_msgs in example (as : List α) : as as := [], by simp

In Notation below, we will discuss Lean's mechanisms for defining notation, and see that they also support the local modifier. However, in Setting Options, we will discuss Lean's mechanisms for setting options, which does not follow this pattern: options can only be set locally, which is to say, their scope is always restricted to the current section or current file.

6.6. More on Implicit Arguments🔗

In Implicit Arguments, we saw that if Lean displays the type of a term t as {x : α} β x, then the curly brackets indicate that x has been marked as an implicit argument to t. This means that whenever you write t, a placeholder, or “hole,” is inserted, so that t is replaced by @t _. If you don't want that to happen, you have to write @t instead.

Notice that implicit arguments are inserted eagerly. Suppose we define a function f : (x : Nat) {y : Nat} (z : Nat) Nat. Then, when we write the expression f 7 without further arguments, it is parsed as @f 7 _.

Lean offers a weaker annotation which specifies that a placeholder should only be added before a subsequent explicit argument. It can be written with double braces, so the type of f would be f : (x : Nat) {{y : Nat}} (z : Nat) Nat. With this annotation, the expression f 7 would be parsed as is, whereas f 7 3 would be parsed as @f 7 _ 3, just as it would be with the strong annotation. This annotation can also be written as ⦃y : Nat⦄, where the Unicode brackets are entered as \{{ and \}}, respectively.

To illustrate the difference, consider the following example, which shows that a reflexive euclidean relation is both symmetric and transitive.

def reflexive {α : Type u} (r : α α Prop) : Prop := (a : α), r a a def symmetric {α : Type u} (r : α α Prop) : Prop := {a b : α}, r a b r b a def transitive {α : Type u} (r : α α Prop) : Prop := {a b c : α}, r a b r b c r a c def Euclidean {α : Type u} (r : α α Prop) : Prop := {a b c : α}, r a b r a c r b c theorem th1 {α : Type u} {r : α α Prop} (reflr : reflexive r) (euclr : Euclidean r) : symmetric r := fun {a b : α} => fun (h : r a b) => show r b a from euclr h (reflr _) theorem th2 {α : Type u} {r : α α Prop} (symmr : symmetric r) (euclr : Euclidean r) : transitive r := fun {a b c : α} => fun (rab : r a b) (rbc : r b c) => euclr (symmr rab) rbc theorem th3 {α : Type u} {r : α α Prop} (reflr : reflexive r) (euclr : Euclidean r) : transitive r := th2 (th1 reflr @euclr) @euclr variable (r : α α Prop) variable (euclr : Euclidean r) euclr : r ?m.460 ?m.461 → r ?m.460 ?m.462 → r ?m.461 ?m.462#check euclr
euclr : r ?m.460 ?m.461 → r ?m.460 ?m.462 → r ?m.461 ?m.462

The results are broken down into small steps: th1 shows that a relation that is reflexive and euclidean is symmetric, and th2 shows that a relation that is symmetric and euclidean is transitive. Then th3 combines the two results. But notice that we have to manually disable the implicit arguments in euclr, because otherwise too many implicit arguments are inserted. The problem goes away if we use weak implicit arguments:

def reflexive {α : Type u} (r : α α Prop) : Prop := (a : α), r a a def symmetric {α : Type u} (r : α α Prop) : Prop := {{a b : α}}, r a b r b a def transitive {α : Type u} (r : α α Prop) : Prop := {{a b c : α}}, r a b r b c r a c def Euclidean {α : Type u} (r : α α Prop) : Prop := {{a b c : α}}, r a b r a c r b c theorem th1 {α : Type u} {r : α α Prop} (reflr : reflexive r) (euclr : Euclidean r) : symmetric r := fun {a b : α} => fun (h : r a b) => show r b a from euclr h (reflr _) theorem th2 {α : Type u} {r : α α Prop} (symmr : symmetric r) (euclr : Euclidean r) : transitive r := fun {a b c : α} => fun (rab : r a b) (rbc : r b c) => euclr (symmr rab) rbc theorem th3 {α : Type u} {r : α α Prop} (reflr : reflexive r) (euclr : Euclidean r) : transitive r := th2 (th1 reflr euclr) euclr variable (r : α α Prop) variable (euclr : Euclidean r) euclr : Euclidean r#check euclr
euclr : Euclidean r

There is a third kind of implicit argument that is denoted with square brackets, [ and ]. These are used for type classes, as explained in the chapter on type classes.

6.7. Notation🔗

Identifiers in Lean can include any alphanumeric characters, including Greek characters (other than ∀ , Σ , and λ , which, as we have seen, have a special meaning in the dependent type theory). They can also include subscripts, which can be entered by typing \_ followed by the desired subscripted character.

Lean's parser is extensible, which is to say, we can define new notation.

Lean's syntax can be extended and customized by users at every level, ranging from basic “mixfix” notations to custom elaborators. In fact, all builtin syntax is parsed and processed using the same mechanisms and APIs open to users. In this section, we will describe and explain the various extension points.

While introducing new notations is a relatively rare feature in programming languages and sometimes even frowned upon because of its potential to obscure code, it is an invaluable tool in formalization for expressing established conventions and notations of the respective field succinctly in code. Going beyond basic notations, Lean's ability to factor out common boilerplate code into (well-behaved) macros and to embed entire custom domain specific languages (DSLs) to textually encode subproblems efficiently and readably can be of great benefit to both programmers and proof engineers alike.

6.7.1. Notations and Precedence🔗

The most basic syntax extension commands allow introducing new (or overloading existing) prefix, infix, and postfix operators.

infixl:65 " + " => HAdd.hAdd -- left-associative infix:50 " = " => Eq -- non-associative infixr:80 " ^ " => HPow.hPow -- right-associative prefix:100 "-" => Neg.neg postfix:max "⁻¹" => Inv.inv

After the initial command name describing the operator kind (its “fixity”), we give the parsing precedence of the operator preceded by a colon :, then a new or existing token surrounded by double quotes (the whitespace is used for pretty printing), then the function this operator should be translated to after the arrow =>.

The precedence is a natural number describing how “tightly” an operator binds to its arguments, encoding the order of operations. We can make this more precise by looking at the commands the above unfold to:

notation:65 lhs:65 " + " rhs:66 => HAdd.hAdd lhs rhs notation:50 lhs:51 " = " rhs:51 => Eq lhs rhs notation:80 lhs:81 " ^ " rhs:80 => HPow.hPow lhs rhs notation:100 "-" arg:100 => Neg.neg arg -- `max` is a shorthand for precedence 1024: notation:1024 arg:1024 "⁻¹" => Inv.inv arg

It turns out that all commands from the first code block are in fact command macros translating to the more general notation command. We will learn about writing such macros below. Instead of a single token, the notation command accepts a mixed sequence of tokens and named term placeholders with precedences, which can be referenced on the right-hand side of => and will be replaced by the respective term parsed at that position. A placeholder with precedence p accepts only notations with precedence at least p in that place. Thus the string a + b + c cannot be parsed as the equivalent of a + (b + c) because the right-hand side operand of an infixl notation has precedence one greater than the notation itself. In contrast, infixr reuses the notation's precedence for the right-hand side operand, so a ^ b ^ c can be parsed as a ^ (b ^ c). Note that if we used notation directly to introduce an infix notation like

notation:65 lhs:65 " ~ " rhs:65 => wobble lhs rhs

where the precedences do not sufficiently determine associativity, Lean's parser will default to right associativity. More precisely, Lean's parser follows a local longest parse rule in the presence of ambiguous grammars: when parsing the right-hand side of a ~ in a ~ b ~ c, it will continue parsing as long as possible (as the current precedence allows), not stopping after b but parsing ~ c as well. Thus the term is equivalent to a ~ (b ~ c).

As mentioned above, the notation command allows us to define arbitrary mixfix syntax freely mixing tokens and placeholders.

notation:max "(" e ")" => e notation:10 Γ " ⊢ " e " : " τ => Typing Γ e τ

Placeholders without precedence default to 0, i.e. they accept notations of any precedence in their place. If two notations overlap, we again apply the longest parse rule:

notation:65 a " + " b:66 " + " c:66 => a + b - c 0#eval 1 + 2 + 3
0

The new notation is preferred to the binary notation since the latter, before chaining, would stop parsing after 1 + 2. If there are multiple notations accepting the same longest parse, the choice will be delayed until elaboration, which will fail unless exactly one overload is type-correct.

6.8. Coercions🔗

In Lean, the type of natural numbers, Nat, is different from the type of integers, Int. But there is a function Int.ofNat that embeds the natural numbers in the integers, meaning that we can view any natural number as an integer, when needed. Lean has mechanisms to detect and insert coercions of this sort. Coercions can be explicitly requested using the overloaded operator.

variable (m n : Nat) variable (i j : Int) i + ↑m : Int#check i + m
i + ↑m : Int
i + ↑m + j : Int#check i + m + j
i + ↑m + j : Int
i + ↑m + ↑n : Int#check i + m + n
i + ↑m + ↑n : Int

6.9. Displaying Information🔗

There are a number of ways in which you can query Lean for information about its current state and the objects and theorems that are available in the current context. You have already seen two of the most common ones, #check and #eval. Remember that #check is often used in conjunction with the @ operator, which makes all of the arguments to a theorem or definition explicit. In addition, you can use the #print command to get information about any identifier. If the identifier denotes a definition or theorem, Lean prints the type of the symbol, and its definition. If it is a constant or an axiom, Lean indicates that fact, and shows the type.

-- examples with equality Eq.{u_1} {α : Sort u_1} : α → α → Prop#check Eq
Eq.{u_1} {α : Sort u_1} : α → α → Prop
@Eq : {α : Sort u_1} → α → α → Prop#check @Eq
@Eq : {α : Sort u_1} → α → α → Prop
Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) : b = a#check Eq.symm
Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) : b = a
@Eq.symm : ∀ {α : Sort u_1} {a b : α}, a = b → b = a#check @Eq.symm
@Eq.symm : ∀ {α : Sort u_1} {a b : α}, a = b → b = a
theorem Eq.symm.{u} : ∀ {α : Sort u} {a b : α}, a = b → b = a := fun {α} {a b} h => h ▸ rfl#print Eq.symm
theorem Eq.symm.{u} : ∀ {α : Sort u} {a b : α}, a = b → b = a :=
fun {α} {a b} h => h ▸ rfl
-- examples with And And (a b : Prop) : Prop#check And
And (a b : Prop) : Prop
And.intro {a b : Prop} (left : a) (right : b) : a ∧ b#check And.intro
And.intro {a b : Prop} (left : a) (right : b) : a ∧ b
@And.intro : ∀ {a b : Prop}, a → b → a ∧ b#check @And.intro
@And.intro : ∀ {a b : Prop}, a → b → a ∧ b
-- a user-defined function def foo {α : Type u} (x : α) : α := x foo.{u} {α : Type u} (x : α) : α#check foo
foo.{u} {α : Type u} (x : α) : α
@foo : {α : Type u_1} → α → α#check @foo
@foo : {α : Type u_1} → α → α
def foo.{u} : {α : Type u} → α → α := fun {α} x => x#print foo
def foo.{u} : {α : Type u} → α → α :=
fun {α} x => x

6.10. Setting Options🔗

Lean maintains a number of internal variables that can be set by users to control its behavior. The syntax for doing so is as follows:

set_option <name> <value>

One very useful family of options controls the way Lean's pretty printer displays terms. The following options take an input of true or false:

pp.explicit  : display implicit arguments
pp.universes : display hidden universe parameters
pp.notation  : display output using defined notations

As an example, the following settings yield much longer output:

set_option pp.explicit true set_option pp.universes true set_option pp.notation false @Eq.{1} Nat (@HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat) (@OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2))) (@OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2)))) (@OfNat.ofNat.{0} Nat (nat_lit 4) (instOfNatNat (nat_lit 4))) : Prop#check 2 + 2 = 4
@Eq.{1} Nat
  (@HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat)
    (@OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2)))
    (@OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2))))
  (@OfNat.ofNat.{0} Nat (nat_lit 4) (instOfNatNat (nat_lit 4))) : Prop
@Eq.{1} (Nat → Nat) (fun x => @HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat) x (@OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2)))) fun x => @HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat) x (@OfNat.ofNat.{0} Nat (nat_lit 3) (instOfNatNat (nat_lit 3)))#reduce (fun x => x + 2) = (fun x => x + 3)
@Eq.{1} (Nat → Nat)
  (fun x =>
    @HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat) x
      (@OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2))))
  fun x =>
  @HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat) x
    (@OfNat.ofNat.{0} Nat (nat_lit 3) (instOfNatNat (nat_lit 3)))
(fun x => @HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat) x (@OfNat.ofNat.{0} Nat (nat_lit 1) (instOfNatNat (nat_lit 1)))) (@OfNat.ofNat.{0} Nat (nat_lit 1) (instOfNatNat (nat_lit 1))) : Nat#check (fun x => x + 1) 1
(fun x =>
    @HAdd.hAdd.{0, 0, 0} Nat Nat Nat (@instHAdd.{0} Nat instAddNat) x
      (@OfNat.ofNat.{0} Nat (nat_lit 1) (instOfNatNat (nat_lit 1))))
  (@OfNat.ofNat.{0} Nat (nat_lit 1) (instOfNatNat (nat_lit 1))) : Nat

The command set_option pp.all true carries out these settings all at once, whereas set_option pp.all false reverts to the previous values. Pretty printing additional information is often very useful when you are debugging a proof, or trying to understand a cryptic error message. Too much information can be overwhelming, though, and Lean's defaults are generally sufficient for ordinary interactions.

6.11. Using the Library🔗

To use Lean effectively you will inevitably need to make use of definitions and theorems in the library. Recall that the import command at the beginning of a file imports previously compiled results from other files, and that importing is transitive; if you import Foo and Foo imports Bar, then the definitions and theorems from Bar are available to you as well. But the act of opening a namespace, which provides shorter names, does not carry over. In each file, you need to open the namespaces you wish to use.

In general, it is important for you to be familiar with the library and its contents, so you know what theorems, definitions, notations, and resources are available to you. Below we will see that Lean's editor modes can also help you find things you need, but studying the contents of the library directly is often unavoidable. Lean's standard library can be found online, on GitHub:

You can see the contents of these directories and files using GitHub's browser interface. If you have installed Lean on your own computer, you can find the library in the lean folder, and explore it with your file manager. Comment headers at the top of each file provide additional information.

Lean's library developers follow general naming guidelines to make it easier to guess the name of a theorem you need, or to find it using tab completion in editors with a Lean mode that supports this, which is discussed in the next section. Identifiers are generally camelCase, and types are CamelCase. For theorem names, we rely on descriptive names where the different components are separated by _s. Often the name of theorem simply describes the conclusion:

Nat.succ_ne_zero (n : Nat) : n.succ ≠ 0#check Nat.succ_ne_zero
Nat.succ_ne_zero (n : Nat) : n.succ ≠ 0
Nat.zero_add (n : Nat) : 0 + n = n#check Nat.zero_add
Nat.zero_add (n : Nat) : 0 + n = n
Nat.mul_one (n : Nat) : n * 1 = n#check Nat.mul_one
Nat.mul_one (n : Nat) : n * 1 = n
Nat.le_of_succ_le_succ {n m : Nat} : n.succ ≤ m.succ → n ≤ m#check Nat.le_of_succ_le_succ
Nat.le_of_succ_le_succ {n m : Nat} : n.succ ≤ m.succ → n ≤ m

Remember that identifiers in Lean can be organized into hierarchical namespaces. For example, the theorem named le_of_succ_le_succ in the namespace Nat has full name Nat.le_of_succ_le_succ, but the shorter name is made available by the command open Nat (for names not marked as protected). We will see in the chapters on inductive types and structures and records that defining structures and inductive data types in Lean generates associated operations, and these are stored in a namespace with the same name as the type under definition. For example, the product type comes with the following operations:

@Prod.mk : {α : Type u_1} → {β : Type u_2} → α → β → α × β#check @Prod.mk
@Prod.mk : {α : Type u_1} → {β : Type u_2} → α → β → α × β
@Prod.fst : {α : Type u_1} → {β : Type u_2} → α × β → α#check @Prod.fst
@Prod.fst : {α : Type u_1} → {β : Type u_2} → α × β → α
@Prod.snd : {α : Type u_1} → {β : Type u_2} → α × β → β#check @Prod.snd
@Prod.snd : {α : Type u_1} → {β : Type u_2} → α × β → β
@Prod.rec : {α : Type u_2} → {β : Type u_3} → {motive : α × β → Sort u_1} → ((fst : α) → (snd : β) → motive (fst, snd)) → (t : α × β) → motive t#check @Prod.rec
@Prod.rec : {α : Type u_2} →
  {β : Type u_3} → {motive : α × β → Sort u_1} → ((fst : α) → (snd : β) → motive (fst, snd)) → (t : α × β) → motive t

The first is used to construct a pair, whereas the next two, Prod.fst and Prod.snd, project the two elements. The last, Prod.rec, provides another mechanism for defining functions on a product in terms of a function on the two components. Names like Prod.rec are protected, which means that one has to use the full name even when the Prod namespace is open.

With the propositions as types correspondence, logical connectives are also instances of inductive types, and so we tend to use dot notation for them as well:

@And.intro : ∀ {a b : Prop}, a → b → a ∧ b#check @And.intro
@And.intro : ∀ {a b : Prop}, a → b → a ∧ b
@And.casesOn : {a b : Prop} → {motive : a ∧ b → Sort u_1} → (t : a ∧ b) → ((left : a) → (right : b) → motive ⋯) → motive t#check @And.casesOn
@And.casesOn : {a b : Prop} →
  {motive : a ∧ b → Sort u_1} → (t : a ∧ b) → ((left : a) → (right : b) → motive ⋯) → motive t
@And.left : ∀ {a b : Prop}, a ∧ b → a#check @And.left
@And.left : ∀ {a b : Prop}, a ∧ b → a
@And.right : ∀ {a b : Prop}, a ∧ b → b#check @And.right
@And.right : ∀ {a b : Prop}, a ∧ b → b
@Or.inl : ∀ {a b : Prop}, a → a ∨ b#check @Or.inl
@Or.inl : ∀ {a b : Prop}, a → a ∨ b
@Or.inr : ∀ {a b : Prop}, b → a ∨ b#check @Or.inr
@Or.inr : ∀ {a b : Prop}, b → a ∨ b
@Or.elim : ∀ {a b c : Prop}, a ∨ b → (a → c) → (b → c) → c#check @Or.elim
@Or.elim : ∀ {a b c : Prop}, a ∨ b → (a → c) → (b → c) → c
@Exists.intro : ∀ {α : Sort u_1} {p : α → Prop} (w : α), p w → Exists p#check @Exists.intro
@Exists.intro : ∀ {α : Sort u_1} {p : α → Prop} (w : α), p w → Exists p
@Exists.elim : ∀ {α : Sort u_1} {p : α → Prop} {b : Prop}, (∃ x, p x) → (∀ (a : α), p a → b) → b#check @Exists.elim
@Exists.elim : ∀ {α : Sort u_1} {p : α → Prop} {b : Prop}, (∃ x, p x) → (∀ (a : α), p a → b) → b
@Eq.refl : ∀ {α : Sort u_1} (a : α), a = a#check @Eq.refl
@Eq.refl : ∀ {α : Sort u_1} (a : α), a = a
@Eq.subst : ∀ {α : Sort u_1} {motive : α → Prop} {a b : α}, a = b → motive a → motive b#check @Eq.subst
@Eq.subst : ∀ {α : Sort u_1} {motive : α → Prop} {a b : α}, a = b → motive a → motive b

6.12. Auto Bound Implicit Arguments🔗

In the previous section, we have shown how implicit arguments make functions more convenient to use. However, functions such as compose are still quite verbose to define. Note that the universe polymorphic compose is even more verbose than the one previously defined.

universe u v w def compose {α : Type u} {β : Type v} {γ : Type w} (g : β γ) (f : α β) (x : α) : γ := g (f x)

You can avoid the universe command by providing the universe parameters when defining compose.

def compose.{u, v, w} {α : Type u} {β : Type v} {γ : Type w} (g : β γ) (f : α β) (x : α) : γ := g (f x)

Lean 4 supports a new feature called auto bound implicit arguments. It makes functions such as compose much more convenient to write. When Lean processes the header of a declaration, any unbound identifier is automatically added as an implicit argument. With this feature we can write compose as

def compose (g : β γ) (f : α β) (x : α) : γ := g (f x) @compose : {β : Sort u_1} → {γ : Sort u_2} → {α : Sort u_3} → (β → γ) → (α → β) → α → γ#check @compose
@compose : {β : Sort u_1} → {γ : Sort u_2} → {α : Sort u_3} → (β → γ) → (α → β) → α → γ

Note that Lean inferred a more general type using Sort instead of Type.

Although we love this feature and use it extensively when implementing Lean, we realize some users may feel uncomfortable with it. Thus, you can disable it using the command set_option autoImplicit false.

set_option autoImplicit false /-- error: unknown identifier 'β' --- error: unknown identifier 'γ' --- error: unknown identifier 'α' --- error: unknown identifier 'β' --- error: unknown identifier 'α' --- error: unknown identifier 'γ' -/ #guard_msgs in def compose (g : β γ) (f : α β) (x : α) : γ := g (f x)

6.13. Implicit Lambdas🔗

When the expected type of an expression is a function that is awaiting implicit arguments, the elaborator automatically introduces the corresponding lambdas. For example, pure's type states that the first argument is an implicit type α, but ReaderT.pure's first agument is the reader monad's context type ρ. It is automatically surrounded with a fun {α} => ..., which allows the elaborator to correctly fill in the implicit arguments in the body.

instance : Monad (ReaderT ρ m) where pure := ReaderT.pure bind := ReaderT.bind

Users can disable the implicit lambda feature by using @ or writing a lambda expression with {} or [] binder annotations. Here are few examples

def id1 : {α : Type} α α := fun x => x def listId : List ({α : Type} α α) := (fun x => x) :: [] -- In this example, implicit lambda introduction has been disabled because -- we use `@` before {kw}`fun` def id2 : {α : Type} α α := @fun α (x : α) => id1 x def id3 : {α : Type} α α := @fun α x => id1 x def id4 : {α : Type} α α := fun x => id1 x -- In this example, implicit lambda introduction has been disabled -- because we used the binder annotation `{...}` def id5 : {α : Type} α α := fun {α} x => id1 x

6.14. Sugar for Simple Functions🔗

Lean includes a notation for describing simple functions using anonymous placeholders rather than fun. When · occurs as part of a term, the nearest enclosing parentheses become a function with the · as its argument. If the parentheses include multiple placeholders without other intervening parentheses, then they are made into arguments from left to right. Here are a few examples:

fun x => x + 1 : Nat → Nat#check (· + 1)
fun x => x + 1 : Nat → Nat
fun x => 2 - x : Nat → Nat#check (2 - ·)
fun x => 2 - x : Nat → Nat
120#eval [1, 2, 3, 4, 5].foldl (· * ·) 1
120
def f (x y z : Nat) := x + y + z fun x1 x2 => f x1 1 x2 : Nat → Nat → Nat#check (f · 1 ·)
fun x1 x2 => f x1 1 x2 : Nat → Nat → Nat
[1, 3, 5]#eval [(1, 2), (3, 4), (5, 6)].map (·.1)
[1, 3, 5]

Nested parentheses introduce new functions. In the following example, two different lambda expressions are created:

fun x => (x, fun x => x + 1) : ?m.2 → ?m.2 × (Nat → Nat)#check (Prod.mk · (· + 1))
fun x => (x, fun x => x + 1) : ?m.2 → ?m.2 × (Nat → Nat)

6.15. Named Arguments🔗

Named arguments enable you to specify an argument for a parameter by matching the argument with its name rather than with its position in the parameter list. If you don't remember the order of the parameters but know their names, you can send the arguments in any order. You may also provide the value for an implicit parameter when Lean failed to infer it. Named arguments also improve the readability of your code by identifying what each argument represents.

def sum (xs : List Nat) := xs.foldl (init := 0) (·+·) 10#eval sum [1, 2, 3, 4]
10
-- 10 example {a b : Nat} {p : Nat Nat Nat Prop} (h₁ : p a b b) (h₂ : b = a) : p a a b := Eq.subst (motive := fun x => p a x b) h₂ h₁

In the following examples, we illustrate the interaction between named and default arguments.

def f (x : Nat) (y : Nat := 1) (w : Nat := 2) (z : Nat) := x + y + w - z example (x z : Nat) : f (z := z) x = x + 1 + 2 - z := rfl example (x z : Nat) : f x (z := z) = x + 1 + 2 - z := rfl example (x y : Nat) : f x y = fun z => x + y + 2 - z := rfl example : f = (fun x z => x + 1 + 2 - z) := rfl example (x : Nat) : f x = fun z => x + 1 + 2 - z := rfl example (unused variable `y` note: this linter can be disabled with `set_option linter.unusedVariables false`y : Nat) : f (y := 5) = fun x z => x + 5 + 2 - z := rfl def g {α} [Add α] (a : α) (b? : Option α := none) (c : α) : α := match b? with | none => a + c | some b => a + b + c variable {α} [Add α] example : g = fun (a c : α) => a + c := rfl example (x : α) : g (c := x) = fun (a : α) => a + x := rfl example (x : α) : g (b? := some x) = fun (a c : α) => a + x + c := rfl example (x : α) : g x = fun (c : α) => x + c := rfl example (x y : α) : g x y = fun (c : α) => x + y + c := rfl

You can use .. to provide missing explicit arguments as _. This feature combined with named arguments is useful for writing patterns. Here is an example:

inductive Term where | var (name : String) | num (val : Nat) | app (fn : Term) (arg : Term) | lambda (name : String) (type : Term) (body : Term) def getBinderName : Term Option String | Term.lambda (name := n) .. => some n | _ => none def getBinderType : Term Option Term | Term.lambda (type := t) .. => some t | _ => none

Ellipses are also useful when explicit arguments can be automatically inferred by Lean, and we want to avoid a sequence of _s.

example (f : Nat Nat) (a b c : Nat) : f (a + b + c) = f (a + (b + c)) := congrArg f (Nat.add_assoc ..)