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.
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.
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
#check double y
#check double (2 * x)
attribute [local simp] Nat.add_assoc Nat.add_comm Nat.add_left_comm
theorem t1 : double (x + y) = double x + double y := by
simp [double]
#check t1 y
#check t1 (2 * x)
theorem t2 : double (x * y) = double x * y := by
simp [double, Nat.add_mul]
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.
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
#check bar
#check Foo.bar
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
-- #check add -- ambiguous
#check String.add -- String → String → String
#check Bool.add -- Bool → Bool → Bool
#check _root_.add -- Type → Type → Type
#check add "hello" "world" -- String
#check add true false -- Bool
#check 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
-- #check bar -- error
#check Foo.bar
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)
#check zero -- Nat
#eval gcd 15 6 -- 3
creates aliases for only the identifiers listed. The command
open Nat hiding succ gcd
#check zero -- Nat
-- #eval gcd 15 6 -- error
#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
#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.
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 Chapter 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 Section 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 :=
⟨[], by simp⟩
example : isPrefix [1, 2, 3] [1, 2, 3] := by
simp
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:
def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
∃ t, l₁ ++ t = l₂
theorem List.isPrefix_self (as : List α) : isPrefix as as :=
⟨[], by simp⟩
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:
def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
∃ t, l₁ ++ t = l₂
section
theorem List.isPrefix_self (as : List α) : isPrefix as as :=
⟨[], by simp⟩
attribute [local simp] List.isPrefix_self
example : isPrefix [1, 2, 3] [1, 2, 3] := by
simp
end
-- Error:
-- example : isPrefix [1, 2, 3] [1, 2, 3] := by
-- simp
For another example, we can use the instance
command to assign the
notation ≤
to the isPrefix
relation. That command, which will
be explained in Chapter 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 :=
⟨[], by simp⟩
That assignment can also be made local:
def isPrefix (l₁ : List α) (l₂ : List α) : Prop :=
∃ t, l₁ ++ t = l₂
def instLe : LE (List α) :=
{ le := isPrefix }
section
attribute [local instance] instLe
example (as : List α) : as ≤ as :=
⟨[], by simp⟩
end
-- Error:
-- example (as : List α) : as ≤ as :=
-- ⟨[], by simp⟩
In Section Notation below, we will discuss Lean's
mechanisms for defining notation, and see that they also support the
local
modifier. However, in Section 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.
More on Implicit Arguments
In Section 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)
with the arguments
shown. Then, when we write the expression f 7
without further
arguments, it is parsed as f 7 _
. Lean offers a weaker annotation,
{{y : Nat}}
, which specifies that a placeholder should only be added
before a subsequent explicit argument. This annotation can also be
written using as ⦃y : Nat⦄
, where the unicode brackets are entered
as \{{
and \}}
, respectively. 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.
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)
#check euclr -- r ?m1 ?m2 → r ?m1 ?m3 → r ?m2 ?m3
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)
#check 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 Chapter Type Classes.
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.
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
set_option quotPrecheck false
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
set_option quotPrecheck false
notation:1024 arg:1024 "⁻¹" => Inv.inv arg -- `max` is a shorthand for precedence 1024
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
set_option quotPrecheck false
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.
set_option quotPrecheck false
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
#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.
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.
variable (m n : Nat)
variable (i j : Int)
#check i + m -- i + Int.ofNat m : Int
#check i + m + j -- i + Int.ofNat m + j : Int
#check i + m + n -- i + Int.ofNat m + Int.ofNat n : Int
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
#check Eq
#check @Eq
#check Eq.symm
#check @Eq.symm
#print Eq.symm
-- examples with And
#check And
#check And.intro
#check @And.intro
-- a user-defined function
def foo {α : Type u} (x : α) : α := x
#check foo
#check @foo
#print foo
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
#check 2 + 2 = 4
#reduce (fun x => x + 2) = (fun x => x + 3)
#check (fun x => x + 1) 1
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.
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:
#check Nat.succ_ne_zero
#check Nat.zero_add
#check Nat.mul_one
#check Nat.le_of_succ_le_succ
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 Chapter Inductive Types
and Chapter 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:
#check @Prod.mk
#check @Prod.fst
#check @Prod.snd
#check @Prod.rec
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:
#check @And.intro
#check @And.casesOn
#check @And.left
#check @And.right
#check @Or.inl
#check @Or.inr
#check @Or.elim
#check @Exists.intro
#check @Exists.elim
#check @Eq.refl
#check @Eq.subst
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 if it is a single lower case or
greek letter. With this feature we can write compose
as
def compose (g : β → γ) (f : α → β) (x : α) : γ :=
g (f x)
#check @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
/- The following definition produces `unknown identifier` errors -/
-- def compose (g : β → γ) (f : α → β) (x : α) : γ :=
-- g (f x)
Implicit Lambdas
In Lean 3 stdlib, we find many
instances of the dreadful @
+_
idiom.
It is often used when the expected type is a function type with implicit arguments,
and we have a constant (reader_t.pure
in the example) which also takes implicit arguments. In Lean 4, the elaborator automatically introduces lambdas
for consuming implicit arguments. We are still exploring this feature and analyzing its impact, but the experience so far has been very positive. Here is the example from the link above using Lean 4 implicit lambdas.
variable (ρ : Type) (m : Type → Type) [Monad m]
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
namespace ex2
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 `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
end ex2
Sugar for Simple Functions
In Lean 3, we can create simple functions from infix operators by
using parentheses. For example, (+1)
is sugar for fun x, x + 1
. In
Lean 4, we generalize this notation using ·
as a placeholder. Here
are a few examples:
namespace ex3
#check (· + 1)
-- fun a => a + 1
#check (2 - ·)
-- fun a => 2 - a
#eval [1, 2, 3, 4, 5].foldl (·*·) 1
-- 120
def f (x y z : Nat) :=
x + y + z
#check (f · 1 ·)
-- fun a b => f a 1 b
#eval [(1, 2), (3, 4), (5, 6)].map (·.1)
-- [1, 3, 5]
end ex3
As in Lean 3, the notation is activated using parentheses, and the lambda abstraction is created by collecting the nested ·
s.
The collection is interrupted by nested parentheses. In the following example, two different lambda expressions are created.
#check (Prod.mk · (· + 1))
-- fun a => (a, fun b => b + 1)
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) (·+·)
#eval sum [1, 2, 3, 4]
-- 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 (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 ..)