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 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.
Note to developers (xhalo32)
This is technically incorrect, the instance BEq Nat, which comes from DecidableEq, does not contain Nat.beq.
You can see in proofs of List.elem_nat versus List.elem_poly_eq versus List.elem_poly
and how Nat.beq and == play different roles.
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:
Now for the marking: we need to tell Lean that DefaultValue is the sort of structure it should
search for automatically, the way it needs to for List.headOrEx's defaultValue argument.
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 1 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. For Nat, that instance is:
Note to developers (@ionathanch)
What is (priority := low)? Do the students need to know why that's there?
instance(priority:=low):BEqNatwherebeq:=Nat.beq
This is the instance Lean supplies for [BEq α] when List.elemPoly is called on a
ListNat — no different from Lean choosing instDefaultValueNat for
DefaultValue.value earlier when it was equated with (1:Nat).
Exercise★(List.elem_poly_eq_elem_nat)
Prove that List.elemPoly agrees with List.elemNat when specialized to
natural numbers.
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.
Note to developers (Niklas Halonen @xhalo32)
HasThree needs either grading attributes or manual grading
Exercise★(HasThree)
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.
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:
theoremid_unique{α:Type}{m₁m₂:Monoidα}(h:m₁.op=m₂.op):m₁.id=m₂.id:=byα:Typem₁:Monoidαm₂:Monoidαh:OpSet.op=OpSet.op⊢ Monoid.id=Monoid.idobtain@⟨op₁,id₁,left_id₁,right_id₁,assoc₁⟩:=m₁α:Typem₂:Monoidαop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.op⊢ Monoid.id=Monoid.idobtain@⟨op₂,id₂,left_id₂,right_id₂,assoc₂⟩:=m₂α:Typeop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zop₂:OpSetαid₂:αleft_id₂:∀(x:α),id₂⊗x=xright_id₂:∀(x:α),x⊗id₂=xassoc₂:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.op⊢ Monoid.id=Monoid.idhaveh':id₁=id₂:=by-- this introduces a use of m₁'s operatorrw[←left_id₁id₂α:Typeop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zop₂:OpSetαid₂:αleft_id₂:∀(x:α),id₂⊗x=xright_id₂:∀(x:α),x⊗id₂=xassoc₂:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.op⊢ id₁=id₁⊗id₂]α:Typeop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zop₂:OpSetαid₂:αleft_id₂:∀(x:α),id₂⊗x=xright_id₂:∀(x:α),x⊗id₂=xassoc₂:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.op⊢ id₁=id₁⊗id₂-- we use our hypothesis to rewrite m₁'s operator into m₂'srw[hα:Typeop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zop₂:OpSetαid₂:αleft_id₂:∀(x:α),id₂⊗x=xright_id₂:∀(x:α),x⊗id₂=xassoc₂:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.op⊢ id₁=id₁⊗id₂]α:Typeop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zop₂:OpSetαid₂:αleft_id₂:∀(x:α),id₂⊗x=xright_id₂:∀(x:α),x⊗id₂=xassoc₂:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.op⊢ id₁=id₁⊗id₂-- then, we can use m₂'s right idrw[right_id₂α:Typeop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zop₂:OpSetαid₂:αleft_id₂:∀(x:α),id₂⊗x=xright_id₂:∀(x:α),x⊗id₂=xassoc₂:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.op⊢ id₁=id₁]α:Typeop₁:OpSetαid₁:αleft_id₁:∀(x:α),id₁⊗x=xright_id₁:∀(x:α),x⊗id₁=xassoc₁:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zop₂:OpSetαid₂:αleft_id₂:∀(x:α),id₂⊗x=xright_id₂:∀(x:α),x⊗id₂=xassoc₂:∀(xyz:α),x⊗y⊗z=(x⊗y)⊗zh:OpSet.op=OpSet.oph':id₁=id₂⊢ Monoid.id=Monoid.id-- the goal `m₁.id = m₂.id` is equivalent with `id₁ = id₂` even though it displays `Monoid.id = Monoid.id`exacth'All goals completed! 🐙
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 however, because these are
class instances instead of normal structures, we prepend our tuple by the @ symbol.
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:
Taking suggestions for additional simple group theory theorems to prove here.
Exercise★(IdentityUnique)
If an element of a monoid satisfies just one of the the identity laws
(here, we take the left), then it must be equal to the monoid's identity element.
Here, we should tie back the story from early chapters about characterizing lemmas and definition unfolding.
When unfolding a definition directly without characterizing lemmas, the implementation details are exposed.
When downstream code can depend on implementation details of upstream library code, it makes it more difficult for the upstream library to evolve.
Explain the following items:
What is API and how does it relate to typeclasses
What is encapsulation: public and private API
Function definitions and one-field structures are encapsulation boundaries
Definitions and structures are usually private, characterizing lemmas are public
Constructors of inductives are public
Mention public, private keywords and that we don't use them on the course?
One can mostly ignore proof terms due to proof irrelevance
Here is an example where the proof term is blocking a rewrite.
The solution is to simplify it away.
-- set_option pp.proofs true inexample{nm:Nat}{a:Finn}{b:Finm}(h₁:n=m)(h₂:a.val=b.val):a=⟨b.val,h₁▸b.isLt⟩:=byα:Typeβ:TypedefaultValue:αn✝:Natm✝:Natn:Natm:Nata:Finnb:Finmh₁:n=mh₂:↑a=↑b⊢ a=⟨↑b,⋯⟩-- ext-- dsimp onlyrw[Tactic `rewrite` failed: motive is not type correct:fun_a=>a=⟨_a,⋯⟩Error: Application type mismatch: The argumenth✝has type_a<mbut is expected to have type↑b<min the applicationEq.ndrec(motive:=fun{m}=>∀{b:Finm},↑a=↑b→↑b<m→↑b<n)(fun{b}h₂h=>h)h₁h₂h✝Explanation: The rewrite tactic rewrites an expression 'e' using an equality 'a = b' by the following process. First, it looks for all 'a' in 'e'. Second, it tries to abstract these occurrences of 'a' to create a function 'm := fun _a => ...', called the *motive*, with the property that 'm a' is definitionally equal to 'e'. Third, we observe that 'congrArg' implies that 'm a = m b', which can be used with lemmas such as 'Eq.mpr' to change the goal. However, if 'e' depends on specific properties of 'a', then the motive 'm' might not typecheck.Possible solutions: use rewrite's 'occs' configuration option to limit which occurrences are rewritten, or use 'simp' or 'conv' mode, which have strategies for certain kinds of dependencies (these tactics can handle proofs and 'Decidable' instances whose types depend on the rewritten term, and 'simp' can apply user-defined '@[congr]' theorems as well).αβ:TypedefaultValue:αn✝m✝nm:Nata:Finnb:Finmh₁:n=mh₂:↑a=↑b⊢ a=⟨↑b,⋯⟩←h₂α:Typeβ:TypedefaultValue:αn✝:Natm✝:Natn:Natm:Nata:Finnb:Finmh₁:n=mh₂:↑a=↑b⊢ a=⟨↑b,⋯⟩]α:Typeβ:TypedefaultValue:αn✝:Natm✝:Natn:Natm:Nata:Finnb:Finmh₁:n=mh₂:↑a=↑b⊢ a=⟨↑b,⋯⟩
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.
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.
To define maps, we first need a type for the keys that we will use to index into our maps and
a type for the values the maps return.
In this section, we'll use the type variable α for the type of keys and β for values.
In addition to BEq, which we have already seen, our key type α requires instances of the
ReflBEq and LawfulBEq typeclasses:
The Lists chapter introduced a partial map abstraction, PartialMap, with a
find function for lookup, based on lists of key-value pairs.
Here, we are going to build 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,
rather than just as "equivalent" list structures. This simplifies proofs that use maps.
Instead of using functions directly, 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 β.
In order to declare a default value of β we will use the Inhabited typeclass,
which is the standard library's implementation of our DefaultValue example from above:
The function TotalMap.empty yields an empty total map, given a default element;
this map always returns the default element when applied to any key.
These types and implicit instances are now available automatically to all the definitions in this section.
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 its public interface.
Accordingly, we define new operations for querying and updating mappings.
We define a function get for getting the value associated with a key playing the role that find played for the Lists chapter's list-based maps,
defget{αβ:Type}(m:TotalMapαβ)(a:α):=m.innera/-- This exposes implementation-specific details of `TotalMap`.
Avoid using this outside the `TotalMap` namespace. -/theoremget_def{αβ:Type}{m:TotalMapαβ}{a:α}:m.geta=m.innera:=byα:Typeβ:Typem:TotalMapαβa:α⊢ m.geta=m.innerarflAll goals completed! 🐙example:emptyNatMap.get2=0:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ emptyNatMap.get2=0rflAll goals completed! 🐙
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.
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.
To make element-getting more convenient, let's define notation so we can write
emptyNatMap[2] rather than emptyNatMap.get. 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.
Note to developers (Niklas Halonen @xhalo32)
A reason I can come up with why we use a notation typeclass (in library code) over a plain notation is that
it makes it possible (for a downstream consumer) to write open scoped MyGetElem
instead of writing open scoped TotalMap and open scoped PartialMap individually.
This wouldn't be a good sell if →ₜ and →ₚ are scoped notations, but they're currently global.
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'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.
Don't worry about following the mechanism in detail — the
macro_rules and the app_unexpander below are minor technicalities. However,
if you do wish to learn more, Chapter 5 and 6 of
Metaprogramming in Lean 4
contain more detail.
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.
Note to developers (Benjamin Pierce @bcpierce00)
Make sure we've really explained open scoped somewhere...
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.
Note to developers (Benjamin Pierce @bcpierce00)
Should we introduce this notation earlier? (Are there good places to use it earlier?)
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.
notationa:55" →ₜ "b:55" ; "m:55=>TotalMap.updatemab/-- This exposes implementation-specific details of `TotalMap`.
Avoid using this outside the `TotalMap` namespace.
Prefer `update_apply` if possible. -/theoremupdate_def{αβ:Type}[BEqα](m:TotalMapαβ)(a:α)(b:β):a→ₜb;m={inner:=funa'=>bifa==a'thenbelsem[a']}:=byα:Typeβ:Typeinst✝:BEqαm:TotalMapαβa:αb:β⊢ a→ₜb;m={inner:=funa'=>bifa==a'thenbelsem[a']}rflAll goals completed! 🐙theoremupdate_apply{αβ:Type}[BEqα](m:TotalMapαβ)(aa':α)(b:β):(a→ₜb;m)[a']=bifa==a'thenbelsem[a']:=byα:Typeβ:Typeinst✝:BEqαm:TotalMapαβa:αa':αb:β⊢ (a→ₜb;m)[a']=bifa==a'thenbelsem[a']rflAll goals completed! 🐙
We can omit the map from the notation when we want it to be empty:
Let's also see a couple of examples of working with updated maps using rewrites:
example:exampleMap'["bar"]=true:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ exampleMap'["bar"]=truerw[exampleMap',α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("bar"→ₜtrue;"foo"→ₜtrue)["bar"]=trueupdate_apply,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ (bif"bar"=="bar"thentrueelse("foo"→ₜtrue)["bar"])=trueBEq.rfl,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ (biftruethentrueelse("foo"→ₜtrue)["bar"])=true`cond_true` has been deprecated: Use `Bool.cond_true` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biftruethenaelseb)=ainstead of∀{α:Sort u_1}(ab:α),(biftruethenaelseb)=acond_trueα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ true=true]All goals completed! 🐙example:exampleMap'["foo"]=true:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ exampleMap'["foo"]=truerw[exampleMap',α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("bar"→ₜtrue;"foo"→ₜtrue)["foo"]=trueupdate_apply,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ (bif"bar"=="foo"thentrueelse("foo"→ₜtrue)["foo"])=trueshow("bar"=="foo")=falsebyα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ exampleMap'["foo"]=truesimpAll goals completed! 🐙,`cond_false` has been deprecated: Use `Bool.cond_false` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biffalsethenaelseb)=binstead of∀{α:Sort u_1}(ab:α),(biffalsethenaelseb)=bcond_falseα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("foo"→ₜtrue)["foo"]=true]α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("foo"→ₜtrue)["foo"]=truerw[update_apply,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ (bif"foo"=="foo"thentrueelse∅["foo"])=trueBEq.rfl,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ (biftruethentrueelse∅["foo"])=true`cond_true` has been deprecated: Use `Bool.cond_true` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biftruethenaelseb)=ainstead of∀{α:Sort u_1}(ab:α),(biftruethenaelseb)=acond_trueα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ true=true]All goals completed! 🐙example:exampleMap'["quux"]=false:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ exampleMap'["quux"]=falserw[exampleMap',α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("bar"→ₜtrue;"foo"→ₜtrue)["quux"]=falseupdate_apply,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ (bif"bar"=="quux"thentrueelse("foo"→ₜtrue)["quux"])=falseshow("bar"=="quux")=falsebyα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ exampleMap'["quux"]=falsesimpAll goals completed! 🐙,`cond_false` has been deprecated: Use `Bool.cond_false` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biffalsethenaelseb)=binstead of∀{α:Sort u_1}(ab:α),(biffalsethenaelseb)=bcond_falseα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("foo"→ₜtrue)["quux"]=false]α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("foo"→ₜtrue)["quux"]=falserw[update_apply,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ (bif"foo"=="quux"thentrueelse∅["quux"])=falseshow("foo"=="quux")=falsebyα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ exampleMap'["quux"]=falserflAll goals completed! 🐙,`cond_false` has been deprecated: Use `Bool.cond_false` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biffalsethenaelseb)=binstead of∀{α:Sort u_1}(ab:α),(biffalsethenaelseb)=bcond_falseα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ∅["quux"]=false]α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ∅["quux"]=falserw[empty_def,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ {inner:=funx=>default}["quux"]=falsegetElem_def,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ {inner:=funx=>default}.get"quux"=falseget_def,α:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ {inner:=funx=>default}.inner"quux"=falseBool.default_boolα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ {inner:=funx=>false}.inner"quux"=false]All goals completed! 🐙
When we use maps in later volumes, we'll need several fundamental facts about how they behave.
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.)
First, the empty map returns its default element for all keys:
Notice that in the example exampleMap'["quux"] = false the last rewrite is effectively just getElem_empty.
Next, if we update a map m at a key a with a new value b and then look up a in the map resulting from the update, we get back b:
@[simp]theoremupdate_eq{αβ:Type}[BEqα][ReflBEqα](m:TotalMapαβ)(a:α)(b:β):(a→ₜb;m)[a]=b:=byα:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ (a→ₜb;m)[a]=brw[update_def,α:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ {inner:=funa'=>bifa==a'thenbelsem[a']}[a]=bgetElem_def,α:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ {inner:=funa'=>bifa==a'thenbelsem[a']}.geta=bget_defα:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ {inner:=funa'=>bifa==a'thenbelsem[a']}.innera=b]α:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ {inner:=funa'=>bifa==a'thenbelsem[a']}.innera=bdsimponlyα:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ (bifa==athenbelsem[a])=b-- reduces `{ inner := ... }.inner` so that we get a subterm that looks like `a == a`rw[BEq.rfl,α:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ (biftruethenbelsem[a])=b`cond_true` has been deprecated: Use `Bool.cond_true` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biftruethenaelseb)=ainstead of∀{α:Sort u_1}(ab:α),(biftruethenaelseb)=acond_trueα:Typeβ:Typeinst✝¹:BEqαinst✝:ReflBEqαm:TotalMapαβa:αb:β⊢ b=b]All goals completed! 🐙
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:
Exercise★★(update_neq) (Optional)
@[simp]theoremupdate_neq{αβ:Type}[BEqα][LawfulBEqα]{m:TotalMapαβ}{a₁a₂:α}(h:a₁≠a₂)(b:β):(a₁→ₜb;m)[a₂]=m[a₂]:=byα:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ (a₁→ₜb;m)[a₂]=m[a₂]solution!rw[update_def,α:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ {inner:=funa'=>bifa₁==a'thenbelsem[a']}[a₂]=m[a₂]getElem_def,α:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ {inner:=funa'=>bifa₁==a'thenbelsem[a']}.geta₂=m[a₂]get_defα:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ {inner:=funa'=>bifa₁==a'thenbelsem[a']}.innera₂=m[a₂]]α:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ {inner:=funa'=>bifa₁==a'thenbelsem[a']}.innera₂=m[a₂]dsimponlyα:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ (bifa₁==a₂thenbelsem[a₂])=m[a₂]rw[beq_false_of_neh,α:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ (biffalsethenbelsem[a₂])=m[a₂]`cond_false` has been deprecated: Use `Bool.cond_false` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biffalsethenaelseb)=binstead of∀{α:Sort u_1}(ab:α),(biffalsethenaelseb)=bcond_falseα:Typeβ:Typeinst✝¹:BEqαinst✝:LawfulBEqαm:TotalMapαβa₁:αa₂:αh:a₁≠a₂b:β⊢ m[a₂]=m[a₂]]All goals completed! 🐙
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.
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:
Two things the Rocq source says here have been dropped.
Rocq frames this case analysis around destruct (eqb_spec x1 x2), which
"simultaneously performs case analysis on the result of String.eqb x1 x2 and
generates hypotheses about the equality (in the sense of =) of x1 and x2"
— the boolean/propositional reflection idiom. The paragraph above replaces that
with by_cases/subst, which is what the Lean proof uses. But
reflection is what the Reflection section below is about, so the two
may want to be connected rather than have one silently displace the other.
Rocq then says "With the example in chapter IndProp as a template, use
String.eqb_spec to prove ...". That cross-reference is dropped, since it is
unclear what the Lean IndProp chapter will end up containing. Revisit later.
Note to developers (Niklas Halonen @xhalo32)
Regarding reflection: I have used show ("bar" == "foo") = false by simp in some of the above sections which would be good to explain in more detail in the reflection section.
The BEq instance is derived from DecidableEq in instBEqOfDecidableEq.
All of the following proofs of the fact use decide internally.
-- set_option trace.Meta.synthInstance true in-- set_option trace.Meta.whnf true inexample:("bar"=="foo")=false:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("bar"=="foo")=falsesimpAll goals completed! 🐙-- goes through a simproc `String.reduceBEq` which reduces to `decide`example:("bar"=="foo")=false:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("bar"=="foo")=falserflAll goals completed! 🐙-- ends up calling `decide ("bar" = "foo")`example:("bar"=="foo")=false:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ ("bar"=="foo")=falsedecideAll goals completed! 🐙
Similarly, prove one final property of the update function:
if we update a map m at two distinct keys,
it doesn't matter in which order we do the updates.
Note to developers (mwhicks1, NOW)
Rocq says "Similarly, use String.eqb_spec to prove ..."; the instruction to use
a specific lemma is dropped here for the same reason as in the note above.
The Rocq source also has getElem_empty (originally apply_empty) and update_eq as (optional)
exercises; here they are worked examples, since update_eq was already
presented that way. Reconsider if this section is rebalanced.
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.
/--
A key-value pair with `↦` syntax.
-/@[ext]structureKVPair(K:Type)(V:Type)wherekey:Kvalue:VnamespaceKVPairscopednotationk" ↦ "v=>KVPair.mkkvendKVPairopenscopedKVPair
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
We should spend some time discussing differences between the inductive approach in Lists.lean and the approach here.
The inductive approach could be made polymorphic and proven to be equivalent with partial maps (I believe), so the point is not that the maps are extensionally different.
A question (that I don't have an answer to) is then: what makes the new partial map better?
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.
structurePartialMap(α:Type)(β:Type)where/-- The underlying total map. Lean always generates a public projection for a structure
field, so `inner` is technically accessible, but it isn't part of the intended interface:
use `PartialMap.toTotal` instead, so there's exactly one sanctioned way to get at it. -/inner:TotalMapα(Optionβ)/- Note that this 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. -/instance{αβ:Type}:EmptyCollection(PartialMapαβ)whereemptyCollection:={inner:=∅}namespacePartialMapdeftoTotal{αβ:Type}(m:PartialMapαβ):TotalMapα(Optionβ):=m.inner/-- This exposes implementation-specific details of `PartialMap`.
Avoid using this outside the `PartialMap` namespace. -/theoremtoTotal_def{αβ:Type}(m:PartialMapαβ):m.toTotal=m.inner:=byα:Typeβ:Typem:PartialMapαβ⊢ m.toTotal=m.innerrflAll goals completed! 🐙instance{αβ:Type}:MyGetElem(PartialMapαβ)α(Optionβ)wheregetElemma:=m.toTotal[a]theoremgetElem_def{αβ:Type}(m:PartialMapαβ)(a:α):m[a]=m.toTotal[a]:=rfldefemptyNatMap:PartialMapNatNatwhereinner:=∅example:emptyNatMap[1]=default:=byα:Typeβ:TypedefaultValue:αn:Natm:Nat⊢ emptyNatMap[1]=defaultrflAll goals completed! 🐙example{n:Nat}:emptyNatMap[n]=none:=byα:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ emptyNatMap[n]=nonerw[getElem_def,α:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ emptyNatMap.toTotal[n]=nonetoTotal_def,α:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ emptyNatMap.inner[n]=noneemptyNatMapα:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ {inner:=∅}.inner[n]=none]α:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ {inner:=∅}.inner[n]=nonedsimponlyα:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ ∅[n]=nonerw[TotalMap.getElem_def,α:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ ∅.getn=noneTotalMap.get_def,α:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ ∅.innern=noneTotalMap.empty_def,α:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ {inner:=funx=>default}.innern=noneOption.default_eq_noneα:Typeβ:TypedefaultValue:αn✝:Natm:Natn:Nat⊢ {inner:=funx=>none}.innern=none]All goals completed! 🐙@[simp]theoremtoTotal_eq_getElem{αβ:Type}(m:PartialMapαβ)(a:α):m.toTotal[a]=m[a]:=rfl
We previously defined TotalMap.get so that users can retrieve elements
from a TotalMap in a manner independent of its actual implementation,
which is a function stored in TotalMap.inner.
We follow a similar principle with PartialMaps,
and define PartialMap.toTotal to be the public API counterpart to PartialMap.inner.
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].
Updating a partial map at a key means storing a some value there.
To update, 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.
In this section, we will make use of some definitions and theorems
about natural numbers that we discussed and proved in previous chapters;
copy your solutions to those problems here:
We've seen two different ways of expressing logical claims in Lean: with booleans (of type
Bool), and with propositions (of type Prop).
Here are the key differences between Bool and Prop:
⠀
Bool
Prop
decidable?
yes
no
useable with match?
yes
no
works with rewrite tactic?
no
yes
The crucial difference between the two worlds is decidability. Every (closed) Lean expression of
type Bool can be simplified in a finite number of steps to either true or false
— i.e., there is a terminating mechanical procedure for deciding whether or not it is true.
This means that, for example, the type Nat→Bool is inhabited only by functions that, given a
Nat, always yield either true or false in finite time; and this, in turn,
means (by a standard computability argument) that there is no function in Nat→Bool
that checks whether a given number is the code of a terminating Turing machine.
By contrast, the type Prop includes both decidable and undecidable mathematical propositions; in
particular, the type Nat→Prop does contain functions representing properties like
"the nth Turing machine halts." The second row in the table follows directly from this essential
difference. To evaluate a pattern match (or conditional) on a boolean, we need to know whether the
scrutinee evaluates to true or false; this only works for Bool, not Prop.
The third row highlights an important practical difference: equality functions like Nat.beq
that return a boolean cannot be used directly to justify rewriting with the rewrite tactic;
propositional equality is required for this. Since Prop includes both decidable and undecidable
properties, we have two options when we want to formalize a property that happens to be decidable:
we can express it either as a boolean computation or as a function into Prop.
theoremeven_iff_Even{n:Nat}:evenn=true↔Evennwheremph:=byn:Nath:evenn=true⊢ Evennhave⟨k,hk⟩:=even_double_existsnn:Nath:evenn=truek:Nathk:n=bifevennthendoublekelsedoublek+1⊢ Evennrw[h,n:Nath:evenn=truek:Nathk:n=biftruethendoublekelsedoublek+1⊢ Evenn`cond_true` has been deprecated: Use `Bool.cond_true` insteadNote: The updated constant has a different type:∀{α:Sort u}{ab:α},(biftruethenaelseb)=ainstead of∀{α:Sort u_1}(ab:α),(biftruethenaelseb)=acond_truen:Nath:evenn=truek:Nathk:n=doublek⊢ Evenn]athkn:Nath:evenn=truek:Nathk:n=doublek⊢ Evennsubsthkk:Nath:even(doublek)=true⊢ Even(doublek)existskAll goals completed! 🐙mprh:=byn:Nath:Evenn⊢ evenn=trueobtain⟨k,hk⟩:=hn:Natk:Nathk:n=doublek⊢ evenn=truesubsthkk:Nat⊢ even(doublek)=trueexacteven_doublekAll goals completed! 🐙endNat
In view of this theorem, we can say that the boolean computation Nat.evenn is reflected
in the truth of the proposition ∃(k:Nat),n=Nat.doublek.
Similarly, to state that two numbers n and m are equal, we can say either
that n==m returns true, or
that n=m
Again, these two notions are equivalent:
Note to developers
This proof is from the typeclass version, which makes more sense if maps are included — CGH
example(n₁n₂:Nat):n₁==n₂↔n₁=n₂:=beq_iff_eq
So what should we do in situations where some claim could be formalized as either a proposition or a boolean computation? Which should we choose?
In general, both can be useful. Which we choose has to do with the computational nature of Lean's
core language, which is designed so that every function it expresses is total, and by default
computable unless we explicit indicate otherwise. As an example, consider
trying to write a function α→α→Bool checking for equality on an arbitrary type:
Note to developers
dsainati12 days ago
We use regular if for the first time here. It is probably necessary to explain at this point what if is and how it differs from bif.
👍
1
berberman1 day ago
Should we clarify the difference between = and ==? Observably if and bif can possibly accept both as the condition because of Coe or DecidableEq instances, which IMO could be confusing.
Probably we can talk a bit about the coercion system in this file as well, since Coe could be an example of typeclasses, so long as if we ignore the outParam thing...
chenson20181 day ago
I definitely intended for this to cover = versus ==. Maps uses LawfulBEq (which says = and == coincide). If that will now appear here it's a good place to give some more detail?
rogerburtonpatel1 day ago
I think right after this part on decidability is good. It's a hefty chunk of information already, so keeping distinct ideas distinct is more likely than not a good call.
Note to developers
@dsainati - commenting this out for the same reason as above
@ionathanch - this needs to be put back in, right? nat_eq continues on referring to this
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
Lean will complain here that it cannot find an instance of Decidable. This typeclass
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
is the way that we express in Lean that a given proposition is decidable. This is the generalization
of our observation that Nat.even_iff_Even was reflecting a proof between boolean and
propositional equality. In fact, we can use this theorem to directly construct a Decidable
instance.
In general, Lean will try to use typeclass synthesis with Decidable in order to determine
when it is appropriate to use Prop and Bool interchangeably.
For instance, while our example eq failed above while trying to use propositional equality =
in the condition of an if statement, we are allowed to write
defnat_eq(mn:Nat):Bool:=ifm=nthentrueelsefalse
Why is this allowed? It is precisely because equality of natural numbers is decidable, and Lean
makes use of this fact. If we print this definition with notation unset, we would find that it
is using instDecidableEqNat:
This is only half the story however: while Lean's core theory enables this computation, Lean is
also often used in applications where we don't care about computability, such as pure mathematics.
In particular, it is possible to write a function for arbitrary equality:
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.
Below are some stray examples from IndProp. Decidable only carries the proposition and not the
boolean, so one direction of reflect_iff is easily translated, but the other is a bit different.
I list some theorems below but you should Loogle and see if that's what you want. Some the the
proofs can be a bit advanced if you follow core, or otherwise a bit circular. — CGH
Burtonpatel: Some more examples would be good. It might be good to start with Nat and then move to the Indprop ones. This is a short chapter, so 5-6 well-chosen, informative exercises could easily fit.
endReflection
Source revision: 9e5dba0, committed 2026-09-25 03:12 UTC