If you prefer to learn more about tactics, here is a practice file on advanced tactics.
So far, we have used dependent type theory as a syntax in which to express mathematical statements such as the following.
theorem FLT {n : Nat} (x y z : Int) :
(n > 2) → x ^ n + y ^ n = z ^ n → x * y * z = 0 := sorry
But we can also use it to represent mathematical objects such as groups, rings, or topological spaces.
To do this in a programming language such as Lean, it is useful to first have a sense of what a record is.
As a first approximation, you can think of a record type as an inductive type with only one constructor. The terms of a record type represent what we think of as tuples.
In Lean, record types are introduced via the keyword structure.
The product of two types, for instance, can be defined as a structure.
structure Prod (X : Type) (Y : Type) where
mk :: (x : X) (y : Y)
The declaration as an inductive type used quite similar syntax:
inductive Prod (X : Type) (Y : Type) where
| mk (x : X) (y : Y) : Prod X Y
While valid, the previous syntax for declaring Prod X Y as a record is not very enlightening. Recall:
structure Prod (X : Type) (Y : Type) : Type where
mk :: (x : X) (y : Y)
Try instead:
structure Prod (X : Type) (Y : Type) : Type where
mk :: -- indicating the constructor's name is optional (try it!)
fst : X
snd : Y
Here, Prod is presented as a structure with two fields, named fst and snd.
Record types come equipped with projections to their fields:
#check Prod.fst -- Prod.fst : {X Y : Type} → Prod X Y → X
#check Prod.snd -- Prod.snd : {X Y : Type} → Prod X Y → Y
The name of the field should reflect that: Prod.fst is much more expressive than Prod.x as a name for the first projection.
A convenient feature of these projections is that you can use dot notation.
#check (2, -1) -- (2, -1) : Nat × Int
#check (2, -1).fst -- (2, -1).fst : Nat
#eval (2, -1).fst -- 2
In mathematics, a monoid is a triple
Since a monoid is some kind of tuple, it is natural to translate this directly into a record type in Lean. We just have to unpack the information about
The type of monoids can be introduced as follows in Lean.
structure Monoid where
carrier : Type
op : carrier → carrier → carrier
assoc : ∀ x y z : carrier, op (op x y) z = op x (op y z)
elt : carrier
neutral : ∀ x : carrier, (op elt x = x) ∧ (op x elt = x)
Note how the field op depends on the field carrier and how the fields assoc and neutral depend on the fields carrier, op and elt.
Also note that the proof of the associativity property is part of the definition 🤯. Same for the proof of the fact that elt is a neutral element.
Concretely, how do we construct a monoid? We must supply a term for each field of the Monoid structure.
def NatAddZero : Monoid where
carrier := Nat
op := Nat.add
assoc := Nat.add_assoc
elt := Nat.zero
neutral := fun (n : Nat) ↦ ⟨Nat.zero_add n, Nat.add_zero n⟩
Note the use of the where keyword, after which we can specify each field separately. The constructor Monoid.mk does not appear anywhere.
This works because Nat.add, Nat.add_assoc, etc are already contained in Lean's standard library. The term you need can be defined there directly (see neutral above).
You can also use a tactic program to write terms that go into the various fields.
def NatAddZero : Monoid where
carrier := by exact Nat
... (omitted)
neutral := by
intro n
constructor
exact Nat.zero_add n
exact Nat.add_zero n
Or, without the where keyword (try it!):
def NatAddZero : Monoid := by constructor; exact Nat; ... (omitted)
One could also think of declaring the type of monoids as follows.
structure Monoid where
carrier : Type
op : carrier → carrier → carrier
assoc : ∀ x y z : carrier, op (op x y) z = op x (op y z)
neutral : ∃ elt : carrier, ∀ x : carrier, (op elt x = x) ∧ (op x elt = x)
The issue with this is that it is then unclear how to refer to the neutral element of a monoid, or if it is even possible to do so. In the previous construction, we had for instance NatAddZero.elt = Nat.zero (and that data can be extracted from the def).
This is problematic if we want to write the definition of a group: to add something like ∀ x : carrier, ∃ y : carrier, (op y x = elt) ∧ (op x y = elt), we need a term elt to refer to.
There is a theoretical way out of the issues above (using a definite description operator), but in practice, we define the type of groups as follows, adding extra data (namely, the inverse map). Additional data and properties appear as additional fields in the structure.
structure Group extends Monoid where
inv_map : carrier → carrier
inv_ppty : ∀ x : carrier, (op (inv_map x) x = elt) ∧ (op x (inv_map x) = elt)
There is no need to repeat the fields carrier, op, etc. They are part of the new structure and can be used when adding new fields. This also creates a projection map from Group to Monoid, which "forgets" the additional fields.
#check @Group.toMonoid -- Group.toMonoid : Group → Monoid
Notet that we can also extend the Monoid structure to CommMonoid, by adding a comutativity property.
structure CommMonoid extends Monoid where
comm : ∀ x y : carrier, op x y = op y x
Then we can define commutative groups by extending the previous two structures, without adding any new field.
structure CommGroup extends Group, CommMonoid
This will not work if the two structures have a field with the same name,
Group.toMonoid by hand.μ : Op X is an operation that admits a neutral element, then such an element is unique.Int, the fact that 0 : ℤ is left and right neutral, or the existence of an inverse (the exact? or simp? tactics may be of assistance).⟨M, inv_map, inv_ppty⟩ and ⟨M', inv_map', inv_ppty'⟩ are equal if and only if M = M' as monoids? And what does this mean?inv_map = inv_map', a condition that only makes sense if M and M' have been identified, but then it reduces to the uniqueness of the inverse of an invertible element.Given a function f : Monoid → T and a term G : Group, we might want the expression f G to automatically type-check (without having to write f G.toMonoid). This can be achieved by implementing a coercion.
The automatization is taken care of via a mechanism called type-class inference. By specifying a function coe : Group → Monoid, our expression f G will automatically be interpreted as f (coe G). Here, coe is a function Group → Monoid.
instance : Coe Group Monoid where
coe := Group.toMonoid
The same kind of consideration arises if we extend the Monoid structure to a structure CommMonoid, with an additional field comm : ∀ x y : X, μ x y = μ y x.
Sometimes there is more than one possible coercion, which may cause conflicts known as diamonds if they are not compatible. In the present case, there is no issue, the diagram commutes!
Note that a term M : Monoid is not a type (it is a term of type Monoid). In particular, it does not make sense to write t : M. What would make sense, though, is t : M.carrier, because M.carrier is a type.
So when we say 'Let M be a monoid and let t be an element of M', what we mean is 'Let M be a monoid and let t be an element of M.carrier'. It would be nice, though, to be able to have t : M be parsed automatically as t : M.carrier.
The following coercion implements exactly that:
instanceinstance instCoeSortMonoid : CoeSort Monoid Type where
coe := fun (M : Monoid) ↦ M.carrier
Here, coe is a function Monoid → Type.
α and a structure β extending α, we can use the projection β.toα : β → α as a coercion from β to α. Thanks to a type-class inference mechanism, functions defined on α then become automatically applicable on terms of type β. Similarly, if a structure carrier (or similar) that is a type, we can use the projection α.carrier : α → TypeHere is the practice file on advanced tactics again, as well as a practice file on algebraic structures. I am happy to answer any questions you may have 😊 .
Thank you for your attention!
`comm : op.isCommutative`, where the predicate `Op.isCommutative` is defined by `∀ x y : X, μ x y = μ y x`.