Chapter Poly introduced parametric polymorphism, declaring a type variable with no
constraint on it.
This lets us work with a type like List α, writing functions like
List.reverse and List.length and proofs like List.length_reverse, which use
only the list's structure and never inspect any particular a : α.
Sometimes, though, we want less freedom: rather than leaving α completely generic, we want to
partially specify its behavior. In Lean, this is done through a form of "ad hoc polymorphism" called
typeclasses. The concept originated in Haskell and is analogous to features you may know from
other languages, such as traits in Rust.
What if we want this to work for lists of any element type, not just Nat? Parametric
polymorphism suggests simply replacing Nat with a type variable α,
but that produces a puzzling error:
defList.elemPoly{α:Type}(x:α)(ys:Listα):Bool:=matchyswith|[]=>false|y::ys'=>biffailed to synthesize instance of type classBEqαHint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.x==ythentrueelseelemPolyxys'
failed to synthesize instance of type classBEqαHint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
Lean is trying to use typeclasses to work out how == should behave on a value of type α.
We'll see exactly why shortly; for now, here's one way to sidestep the problem:
have the caller supply the equality test to use.
This works, but it's tedious: every caller has to know, and remember to supply, the right equality
function.
Typeclasses automate this — instead of the programmer passing the function
explicitly, Lean searches for one and provides it on its own. We specify something we want
Lean to search for by declaring a class with the needed function as a field; a class is like
an interface in Java or a trait in Rust. Particular implementations of that class
are called instances; each type can have its own instance of a class. For functions that would
use such instances, we specify
the name of the class in an instance implicit on the polymorphic variable that the
instance's function applies to. This directs Lean to rely on the class inside the function, and to
find and fill in the appropriate instance when the function is called.
Comparing List.elemPolyEq with List.elemPoly, we see three differences.
First, List.elemPolyEq takes an explicit parameter eq,
whereas List.elemPoly specifies an instance implicit [BEq α]. The instance implicit
indicates that an instance of BEq must be provided at
call sites for the particular type α that is used.
Second, whereas List.elemPolyEq invokes the parameter eq to test equality,
List.elemPoly uses == instead. As Lists noted when we first used it,
== on Nat comes from the BEq typeclass.
Finally, whereas [0,1].elemPolyEqNat.beq0 passes the equality
function Nat.beq explicitly, in [0,1].elemPoly0 Lean fills it in
automatically based on the type Nat of the List.
(It's tempting to assume Lean fills in Nat.beq itself here — it doesn't; we'll see exactly
which BEq instance it finds, and why, in "Deciding Propositions" later in this chapter.)
Going back to the earlier version of List.elemPoly, without the instance implicit, we
can now understand the error message: α was fully generic,
so the == in its body would have needed to work for every type α, and no
single BEq instance can do that. So Lean's search failed.
Now it is time to dig into the details of what we have seen so far.
We'll see exactly how [BEq α] gets filled in below, starting with how
to define a typeclass in the first place.
BEq comes from Lean's standard library. Let's define a typeclass of our own, to see the
mechanism — classes, instances, and synthesis — that made == resolve automatically above.
Suppose we want a function that returns the first element of a list, defaulting to a
given value if the list is empty. As with List.elemPolyEq above, here is a version
that makes the default value an explicit parameter:
This works, but again it's tedious: every caller has to supply an element of α to default to,
even when there's an obvious choice based on the type of the things in the list,
like 0 for Nat.
Getting Lean to fill in defaultValue automatically takes two things. One is marking the parameter as
"searchable," rather than something the caller always supplies explicitly. The other is giving Lean
some information about what it should search for.
Considering the second problem first: the way to provide this information is to name the data we're
after — the default value of a type. In particular, a structure (chapter Lists) is a
good way to give this information a name; structures can also bundle together more than one
piece of data, which will come in handy later, though we only need a single field here.
To address the first problem, we need to mark this particular structure as one Lean should search
for automatically — not every structure-typed argument should be.
Let's build up to what we want in two steps: first the naming, as a plain structure; then the marking, by
upgrading it to a class. Here's the structure — we'll put it in its own namespace so we can reuse
the name DefaultValue for the class version below:
A value of type DefaultValueNat picks out a particular Nat to serve as the type's default:
it's built the same way any structure is, by supplying a Nat for the value field:
Naming the data this way already lets us take a step toward what we want: a version of
List.headOrEx can take a DefaultValue α argument instead of a raw α, with callers
supplying a value like natDefault instead of a bare Nat:
This is more verbose than before — callers must build a DefaultValue value first — but it has
the right shape: all that's left is marking DefaultValue as searchable, so Lean can supply this
argument itself. How do we do that?
We need to tell Lean that DefaultValue is the sort of structure it should
search for automatically.
We do this by writing class in place of structure:
classDefaultValue(α:Type)wherevalue:α
We then provide values of this type a bit differently. Instead of def, we use instance:
Lean can now find this instance on its own, via typeclass synthesis (or typeclass inference) —
the same process that found BEqNat earlier. That means we can rewrite List.headOrEx
the same way we rewrote List.elemPolyEq into List.elemPoly above, replacing the
explicit defaultValue parameter with an instance implicit:
Notice that we refer to DefaultValue.value alone, with no instance named. Because the
expression equates DefaultValue.value with the Nat0, Lean selects
instDefaultValueNat, the instance for DefaultValueNat. We know this because we
are able to prove that DefaultValue.value is equal to 0.
Let's declare a second instance, for Int, the type of integers ... -2, -1, 0, 1, 2, ...:
For a typeclass like DefaultValue that carries data — a term, such as the 0 above,
rather than only proofs (which we will see below) — we expect at most one instance per type,
so this search has a unique answer.
We'll put DefaultValue's standard-library equivalent, Inhabited, to work later in this
chapter, when we define maps that need a default value for a generic type. First, though, let's go
back to List.elemPoly and see how its [BEq α] argument actually gets resolved.
Rather than Nat.beq, == turns out to be notation for BEq.beq, a field of exactly
the kind of typeclass we just learned to define:
classBEq(α:Type)wherebeq:α→α→Bool
Writing x == y makes Lean search for an instance of BEq for the type of x and y, the
same way it searched for a DefaultValue instance above. Here is one way to define such an
instance for Nat:
This instance is given low priority so that it doesn't override the standard library's own
BEq Nat instance — which, as we'll see later in this chapter, is actually derived from Nat's
decidable equality rather than from Nat.beq directly. Declaring it here just illustrates
what a hand-written BEq instance looks like, the same way instDefaultValueNat illustrated
a hand-written DefaultValue instance earlier.
If you prefer a specific instance you can provide it explicitly, by using @ to make
the instance argument explicit. Here we provide our instNatbeq instance specifically.
The above examples enforce no conditions on the data an instance may carry — any value of the
right type will do. But sometimes enforcing constraints on data is useful. For example,
suppose we want to specify that a type has not just a single element, but two. Here is a first
attempt:
classHasTwoIncomplete(α:Type)whereone:αtwo:α
Unfortunately, this specification isn't precise because it allows one and two to refer to the
same term. Fortunately, Lean's typeclasses can carry proofs along with data, so we can write the
following to enforce that one and two are distinct.
In most languages that support typeclasses (or traits), it is not possible to formally enforce
laws such as one_neq_two. Thus it falls to the author to check, informally, that any required
invariants are satisfied, which can lead to bugs.
Exercise★(HasThree) (Manually graded)
Following the pattern of DefaultValue and HasTwo, define a class HasThree that
specifies a type with at least three distinct elements, and give an instance of it for
Nat.
classHasThree(α:Type)whereone:αtwo:αthree:αone_neq_two:one≠two-- FILL IN HEREdeclaration uses `sorry`instance:HasThreeNatwhereone:=1two:=2three:=3one_neq_two:=sorry-- FILL IN HERE
namespaceAlgebra
This facility is very powerful, and is used extensively in Lean to define mathematical structures
that carry both operators and laws about how those operators interact. As a simple example,
let's use a typeclass to define a monoid, a simple algebraic structure that includes four things:
an underlying set of data, represented by a type α,
an operator (which we'll write ⊗, typed \otimes) that combines two elements of type α into one,
a particular element id of type α, which we call the "identity element," and
some laws about the interaction of ⊗ and id, namely that:
∀ x, id ⊗ x = x = x ⊗ id, and
∀ x y z, x ⊗ (y ⊗ z) = (x ⊗ y) ⊗ z (i.e., that ⊗ is associative)
We can express these requirements in the form of a typeclass:
-- first we define a notation typeclass for our operator ⊗classOpSet(α:Type)whereop:α→α→αinfixr:70" ⊗ "=>OpSet.opclassMonoid(α:Type)extends(OpSetα)whereid:αleft_id(x:α):id⊗x=xright_id(x:α):x⊗id=xassoc(xyz:α):x⊗(y⊗z)=(x⊗y)⊗z
The extends keyword indicates that the Monoid typeclass extends the OpSet typeclass,
which just defines a set with an operator and some notation for it. The Monoid typeclass
"inherits" the fields of OpSet, similar to how a class would in an object-oriented language.
As one might expect, the + operator over Nats forms a monoid, where 0 is the identity element.
Note that we don't have to define Nat's OpSet instance separately; we can define a
single instance that implements both classes.
There are also many monoids over other types. Most usefully in computer science,
lists of any type also form a monoid, with List.append as the operator in question:
In addition to defining instances of Monoid, we can also prove some properties about
monoids in general, just based on the laws defined on the typeclass. One simple theorem
about monoids is that the identity element of a monoid is unique. That is,
if we have two monoids over the same set with the same operator, their identity elements must also
be the same:
In the above proof, we can destructure the monoid instances m₁ and m₂ with the obtain tactic
we saw in the Logic chapter. When we do so, we prepend the @ symbol to
our tuple so that we can "flatten" the Monoid to reveal both its OpSet field
op and its Monoid (only) fields id, left_id, etc., together.
When stepping through the above proof, if the notation is confusing to you,
remember that you can set set_option pp.all true or set_option pp.explicit true
to make Lean show you more clearly what is going on.
For example, the goal is displayed as Monoid.id = Monoid.id since the instances m₁ and m₂ are
implicit arguments to Monoid.id. Setting pp.explicit true displays the goal as
@Eq α (@Monoid.id α m₁) (@Monoid.id α m₂).
A group is a special kind of monoid with an inverse operation inv, which has the property that
∀ x, inv x ⊗ x = id = x ⊗ inv x. We can extend the definition of a Monoid to capture this
new feature:
Now, the monoids we described earlier are not groups: there is no inverse operation on the
natural numbers such that ∀ x, x + inv x = 0 = inv x + x, for example. However,
addition does form a group over the integers:
Maps (or "dictionaries") are ubiquitous data structures both in ordinary programming and in the
theory of programming languages; we're going to need them in many places in later volumes.
Maps are also where the ideas in this chapter come together in a single, realistic example:
overloaded notation, typeclass-supplied defaults, and proof-carrying instances that guarantee a
data structure behaves the way we expect.
We'll define two flavors of maps: total maps, which include a "default" element to be returned
when a key being looked up doesn't exist, and partial maps, which instead return an option to
indicate success or failure. Partial maps are defined in terms of total maps, using none
as the default element.
The Lists chapter introduced a partial map abstraction, PartialMap, with a
find function for lookup, based on lists of key-value pairs.
Here, we offer a map abstraction using functions instead.
The advantage of this representation is that it offers a more extensional view of maps,
as we saw with functions in the Logic chapter:
two maps that respond to every query in the same way will be represented as exactly the same function.
This simplifies proofs that use maps — we'll see exactly how once we start relating different
updates to each other, below.
We encapsulate them inside a structure which we call TotalMap.
Intuitively, a total map just contains a function inner from a key of type α to a value of type β.
structureTotalMap(α:Type)(β:Type)whereinner:α→β
An "empty" TotalMap is one that maps every α to the same (default) β.
We capture this idea using the Inhabited typeclass,
which is the standard library's version of the DefaultValue class we developed above.
The function TotalMap.empty yields an empty total map with the given default element.
Just as declaring BEq/DefaultValue instances above hooked ==
and DefaultValue.value up to our types, we can declare an instance of the standard
library's EmptyCollection typeclass to associate ∅ with this empty map.
While TotalMaps happen to be implemented as functions under the hood, we would prefer
not to expose this fact in their public interface.
Accordingly, we define a specific function for querying a map,
rather than expecting clients to call a TotalMap's inner function directly.
This function get, for getting the value associated with a key, plays the role that find
played for the Lists chapter's list-based maps.
Function get is the public API counterpart to inner, which is an
implementation-specific detail of TotalMap.
Because get_def "peeks" through the abstraction, it should be used sparingly, and only
inside the TotalMap namespace.
Here is an example that uses the API lemmas empty_def and get_def:
In the above example, we use rewrite and rfl instead of the usual rw
to highlight something interesting. After the rewrites in this proof,
we end up with a goal that looks like { inner := fun x => 0 }.inner n = 0, which we can solve
with rfl. This is because the projection .inner on a structure of the form { inner := x }
is definitionally equal to x.
To make element-getting more convenient, let's define notation so we can write
emptyNatMap[2] rather than emptyNatMap.get2. We could notate get directly —
we'll do exactly that for update below — but here we'll instead make "getting an element" its own
typeclass, MyGetElem, and notate it. Doing so means m[a] resolves to MyGetElem.getElem m a
for any type with a MyGetElem instance, not just TotalMap.
Using typeclasses to define notation is typical in Lean when the same notation is useful
for many different types.
We have seen the approach already with ==: writing
x == y is notation for BEq.beq, resolved by instance search for whatever type x and y
have. We also just saw overloaded notation for EmptyCollection above,
where ∅ is notation for EmptyCollection.emptyCollection.
Our typeclass MyGetElem is a simpler version of the standard library's GetElem typeclass,
which has many instances such as Array, List, and Vector.
We develop it to illustrate the notation-as-typeclass approach.
endTotalMap
The MyGetElem typeclass takes three type parameters: the collection implementation,
its keys, and its values.
Now we can associate the bracket syntax with MyGetElem.getElem. We'll do so with the more
general notation/macro_rules forms, rather than the infixl/infixr/scoped macro forms we've
used for custom notation so far.
Notation encoding: `MyGetElem` brackets
We've defined custom notation before — :: and [...] for lists (chapter Lists,
including an app_unexpander for
printing [...]-notation lists back out), or +/*/== for arithmetic — but always with
infixl/infixr or scoped macro; this is the first time we reach for the more general
notation/macro_rules forms for getting the m[a] syntax to work. Chapters 5 and 6 of
Metaprogramming in Lean 4
contain more detail on this mechanism.
Since the standard library already declares the x[i] syntax for GetElem,
we only need to define the macro_rules, not the notation as we have done previously.
It's scoped since we don't want to override the default GetElem everywhere, but
only when open scoped MyGetElem is in force.
Since we provided a MyGetElem instance for TotalMap, we can now use the
notation m[a] to access elements of a map m.
We want the public API of TotalMap to use the m[a] notation instead of m.get a,
so we provide the reverse direction of getElem_def as a simp lemma;
the m[a] notation is the TotalMap API's simp normal form.
Now we turn to the update function, which takes a map m, a key a, and a value b,
and returns a new map that takes a to b and takes every other key to whatever m does.
We do this by wrapping a new map function around the old one.
Here |> is Lean's pipe notation: x |>.f y means x.f y, letting us chain a sequence of
function or method calls left to right without nested parentheses.
We also introduce a notation for updating maps — this time, rather than going through a typeclass
and its own notation/macro_rules machinery as we did for MyGetElem, we write a notation
that references TotalMap.update directly. Unlike indexing, update doesn't need to work
generically across container types (there's no standard-library operation like GetElem that
we're mirroring here), so the simpler, direct route suffices.
Each have ... = false by simp/by rfl above goes through because String's BEq instance is
ultimately derived from its DecidableEq instance — "Deciding Propositions" below explains this
mechanism in full (worked out there for Nat, but the connection is general).
When we use maps in later volumes, we'll need several fundamental properties about how they behave.
Several of these properties depend on BEq, which our maps use to compare
keys of type α.
These classes refine BEq, specifying that == is reflexive and coincides with
propositional equality =. Neither property is automatic: BEq's only obligation is to
return someBool, with no proof attached, so an arbitrary BEq instance could compute
anything at all, whether or not it agrees with =. We'll need both facts below: reflexivity to
show that looking up the key you just updated returns the new value, and agreement with = to
show that updating one key leaves lookups at every other key unchanged. We'll return to this
distinction between BEq and provable equality in "Deciding Propositions" below.
Even if you don't work the following exercises, make sure you thoroughly understand the statements
of the lemmas! Some of the proofs require the extensionality tactic ext,
discussed in the Logic chapter.
Here is our first property, that the empty map returns its default element for all keys:
On the other hand, if we update a map m at a key a₁ and then look up a different key a₂
in the resulting map, we get the same result that m would have given:
The two remaining facts are equalities between maps, so we first need to say when two maps are
equal. Since a total map is implemented as a function,
this is effectively the functional extensionality principle (funext) from the
Logic chapter: two maps are equal when they agree at every key.
Recording it once, for maps, and tagging it @[ext] lets the ext tactic reduce a
goal m₁ = m₂ to the pointwise one in the proofs below.
The fact that TotalMap is a structure complicates things slightly.
We need to use injectivity of its constructor mk, which Lean automatically provides
for us as mk.injEq. It lets us prove m₁ = m₂ from m₁.inner = m₂.inner or vice versa.
This proof amounts to peeling off the TotalMap wrapper via
mk.injEq, making the goal an equality between the underlying .inner functions. Then the inner
ext call finishes it by invoking funext — the standard library's extensionality
principle for every function type, already proved once and for all, with nothing
TotalMap-specific left to establish. This is the proof-simplifying payoff of representing
maps as functions.
To demonstrate this extensionality principle, let's look at an example:
Given keys a₁ and a₂, the tactic by_casesh : a₁ = a₂
splits the proof into the case where they are equal
— where subst h then replaces one by the other —
and the case where they are not, which is what update_neq wants.
Use it to prove the following theorem,
which states that if we update a map to assign key a the same value as it already has in m,
then the result is equal to m:
Similarly, if we update a map m at a key a with a value b₁
and then update again with the same key a and another value b₂,
the resulting map behaves the same (gives the same result when applied to any key)
as the simpler map obtained by performing just the second update on m:
Wouldn't it be nice if we could use a more natural notation for concrete maps like
{ "bar" ↦ true, "foo" ↦ true }? To accomplish this, we define a simple structure that
consists of a key and a value, along with ↦ notation for it.
Next, we declare Insert and Singleton instances — the standard-library typeclasses behind the
{x, y, ...} and {x} collection-literal notation that List, Finset, and other stdlib
containers already support — so that TotalMap can use it too.
The reason we need to explicitly specify the type of the map is that
Lean doesn't know what type of collection { "foo" ↦ true } is without type hints,
as we can see with #check:
{"foo"↦true} : ?m.2#check{"foo"↦true}
{"foo"↦true} : ?m.2
The type shows a ?m.4, which indicates that Lean can't infer the type.
A type which can't be inferred doesn't have any type classes like MyGetElem,
so typeclass resolution gets stuck in the following example:
example:(typeclass instance problem is stuckSingleton(KVPairStringBool)?m.6Note: Lean will not try to resolve this typeclass instance problem because the second type argument to `Singleton` is a metavariable. This argument must be fully determined before Lean will try to resolve the typeclass.Hint: Adding type annotations and supplying implicit arguments to functions can give Lean more information for typeclass resolution. For example, if you have a variable `x` that you intend to be a `Nat`, but Lean reports it as having an unresolved type like `?m`, replacing `x` with `(x : Nat)` can get typeclass resolution un-stuck.{"foo"↦true})["foo"]=true:=byrfl
Lastly, we define partial maps on top of total maps. A partial map with elements of type β is
simply a total map with elements of type Option β, whose default element is none.
Note that the definition of EmptyCollection doesn't need β to have an Inhabited
instance like TotalMap did. This is because Option β has its own Inhabited
instance: none is a value of every Option type.
Now let's define a PartialMap's operations. We will do so using those of TotalMap
via PartialMap.toTotal.
Note that toTotal_def exposes implementation-specific details of PartialMap.
So we should aovid using this outside the PartialMap namespace. Now we can define the
getElem operation.
We again want the public API to use the m[a] notation (instead of m.toTotal[a]),
so we provide the reverse direction of getElem_def as a simp lemma
to specify that the simp normal form is m[a].
Now let's turn to the update operation.
Updating a partial map at a key means storing a some value there.
So we create a new partial map from a →ₜ some b ; m.toTotal
by wrapping it in angle brackets, i.e., using the anonymous constructor syntax.
This is equivalent to writing { inner := a →ₜ some b ; m.toTotal }.
We also introduce a similar notation for it as for total maps.
Next, we lift all of the basic lemmas about total maps to partial maps.
To do this, we should first prove an extensionality lemma about partial maps.
To prove extensionality, we employ injectivity of PartialMap's constructor mk
using mk.injEq.
One last thing: for partial maps, it's convenient to introduce a notion of map inclusion, stating
that all the entries in one map are also present in another. Lean already has notation for this —
m₁ ⊆ m₂ — which we get by supplying a HasSubset instance.
This property is quite useful for reasoning about languages with variable binding —
e.g., the Simply Typed Lambda Calculus, which we will see in Type Systems,
where maps are used to keep track of which program variables are defined in a given scope.
The Logic chapter's "Working with Decidable Properties" section explored the
trade-offs between stating a claim as a boolean (of type Bool) and as a proposition (of
type Prop). Here, we tie up a loose end from the start of this chapter, and along the way
formalize decidability itself as a typeclass.
Recall from "Why We Need Typeclasses" that [0, 1].elemPoly 0's == is filled in automatically by
Lean, in contrast to [0, 1].elemPolyEq Nat.beq 0, which is handed Nat.beq explicitly.
It's tempting to assume Lean fills in that very Nat.beq function as the required BEq
instance — but it doesn't. We can see this by asking Lean to synthesize the instance directly:
instBEqOfDecidableEq#synthBEqNat
instBEqOfDecidableEq
What is this? Recall that BEq's only field is beq : α → α → Bool.
instBEqOfDecidableEq builds a BEq α instance by setting beq a b := decide (a = b).
What is decide? Let's see:
We can see that decide takes a proposition (like a = b) and a proof that that
proposition is decidable (i.e., Decidable (a = b)), and returns a Bool corresponding
to the truth or falsehood of that proposition. What does it mean for a proposition to
be Decidable? Here is its definition:
classinductiveDecidable(p:Prop)where/-- Proves that `p` is decidable by supplying a proof of `¬ p` -/|isFalse(h:Notp):Decidablep/-- Proves that `p` is decidable by supplying a proof of `p` -/|isTrue(h:p):Decidablep
In other words, Decidable p expresses that a single proposition p can be settled one way or the
other, computationally. (But "computationally" is caveated: later we'll see that classical
axioms can manufacture a Decidable p instance that doesn't compute, and explain what that means.)
Like HasTwo's one_neq_two field earlier in this chapter,
Decidable.isTrue/Decidable.isFalse are proof-carrying: each one packages a
proof — of p or of ¬p — alongside which case holds. instBEqOfDecidableEq references
DecidableEq α, which means ∀ a b : α, Decidable (a = b), i.e., it's a shorthand for
having one of these proof-carrying values for every equality proposition a = b in α.
Because decide (a = b) genuinely computes whether a = b holds, deriving BEq from
DecidableEq this way also guarantees the result is LawfulBEq: the beq it
produces is certain to agree with =. That guarantee is a fact about this instance, though, not
something BEq demands of every instance — as the Maps section noted, beq's only
obligation is to return someBool, with no proof attached, unlike Decidable's constructors,
whose whole point is to carry one. List.elemPoly's [BEq α] constraint is therefore the
weakest assumption sufficient for a purely computational membership test: it doesn't require the
caller to have decidable equality, or even a comparison that agrees with =, at all.
This is also why the chapter's earlier hand-written BEq Nat instance — the low-priority one built
directly from Nat.beq — is a worse choice, not just a redundant one. Nat.beq does happen
to agree with =, but nothing tells Lean that automatically: proving that hand-written instance
is LawfulBEq would take its own separate induction on Nat.beq's recursive definition.
Deriving BEq Nat from DecidableEq Nat sidesteps that work entirely — the proof of
agreement is already carried by the Decidable instance, as we saw above — which is exactly why
the standard library prefers it.
A Decidable instance also allows computation — branching — on the truth/falsehood of a
proposition. You can write if p then a else b where p is a Prop and a and b are
expressions of some type α. For example, we can write
"ok"#evalif2=3then"wrong"else"ok"
"ok"
Here, Lean synthesizes a Decidable instance for the proposition 2=3 which
if (under the covers: ite) case-splits on, returning the then branch's result
on matching the isTrue case and the else branch's result on the isFalse case.
Asking Lean to synthesize the instance for this branched-on proposition shows which one gets used:
So we have landed back at Nat.beq! It computes the result as a Bool and then
Nat.decEq returns a proof of that computation's result in a Decidable instance.
Above, our synthesized Decidable instance was for a particular equality, but
it was just an application of instDecidableEqNat to a particular pair of Nats,
so instDecidableEqNat will get used in the general case, too.
defnat_eq(mn:Nat):Bool:=ifm=nthentrueelsefalse
But if we slightly generalize this function, it will fail.
defeq{α:Type}(xy:α):Bool:=failed to synthesize instance of type classDecidable(x=y)Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.ifx=ythentrueelsefalse
failed to synthesize instance of type classDecidable(x=y)Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
Lean cannot synthesize a decidable equality instance for arbitrary types, and gives the same sort
of error we saw at the beginning of this chapter when defining List.elemPoly. And the
solution is the same too: Add an instance implicit.
Lean provides some automation for proofs of propositions that are Decidable, in
the form of the decide tactic — not to be
confused with the decidefunction from earlier. The function merely computes a Bool
from a Decidable instance; the tactic instead closes a goal p outright, by computing that
decide p reduces to true and invoking the connection between the two (made precise below):
The decide tactic rests on two lemmas relating a Decidable instance's underlying
boolean to the proposition it decides: decide_eq_true_iff says
decide p = true ↔ p, and decide_eq_false_iff_not says
decide p = false ↔ ¬p. decide reduces the goal p to computing whether decide p
evaluates to true, then invokes the first of these.
instDecidableEqNat works automatically because Lean's core library derives it for us — but
not every proposition we might want to decide comes with a ready-made instance.
Sometimes we have to build one ourselves. Let's revisit the Logic chapter's "even"
example to see what that looks like: it stated the property as both Nat.even (a Bool
computation) and Nat.Even (a Prop), connected by reflection via Nat.even_bool_prop.
We restate the relevant pieces here, in their own namespace (dropping the Nat. prefix), so they
don't require that chapter's import:
even_iff_Even (our copied-in version of Nat.even_bool_prop from Logic)
is the kind of iff-shaped reflection lemma we need: the standard
library's decidable_of_decidable_of_iff carries a Decidable instance across any p ↔ q
— from Decidable p to Decidable q. Applying it here is all it takes to build a custom
Decidable (Even n) instance:
Given a Decidable p instance and a proof p ↔ q, it produces a Decidable q. Here, p is
even n = true — already decidable, since equality of Bools always is — and q is
Even n; even_iff_Even supplies the connecting p ↔ q. Under the hood, it checks whether
p holds (using the Decidable p instance it was given) and uses the iff to turn that proof of
p or ¬p into one of q or ¬q, which it then packages with
Decidable.isTrue/Decidable.isFalse.
Now we can complete such proofs by computation, using the decide tactic, and
use Even in if expressions.
Computable instances like instDecidableEqNat and the one we built for Even are only half
the story. Lean is also often used in applications where we don't care about
computability, such as pure mathematics. The Logic chapter's "Classical vs.
Constructive Logic" section already showed how Classical.choice lets Lean prove p ∨ ¬ p for an
arbitrary proposition p via Classical.em, something no computable procedure could do in
general. The same axiom lets Lean manufacture a Decidable p instance for arbitrary p, letting us
write a function for arbitrary equality that no decision procedure could actually compute:
But we have indicated to Lean, using the noncomputable keyword and Classical namespace,
that we are not interested in computation.
What is happening in the background is that this allows
typeclass synthesis to find the scoped instance Classical.propDecidable, which makes use of
the axiom of choice to provide a proof that all propositions are classically decidable. This
sort of definition is suitable for use with proofs, but is not allowed to be used in conjunction
with computational features of Lean such as the decide tactic or the #eval command.
To see this concretely, consider Maps section's TotalMap.update_same exercise,
proves that updating a map with the value already stored there changes nothing, for a fully
generic α constrained only by [BEq α] [LawfulBEq α]. If you use
by_casesh : a = a' in your proof and then do #print axioms update_same
you will see Classical.choice in the list. If you additionally require [DecidableEq α] along
with [BEq α] and [LawfulBEq α], though, then you bring a genuine decision
procedure for equality in scope, and by_cases splits on it directly rather than
falling back to the excluded middle. Try it out!
endReflection
Source revision: e85fe77, committed 2026-10-06 21:16 UTC