Type classes were introduced as a principled way of enabling
ad-hoc polymorphism in functional programming languages. We first observe that it
would be easy to implement an ad-hoc polymorphic function (such as addition) if the
function simply took the type-specific implementation of addition as an argument
and then called that implementation on the remaining arguments. For example,
suppose we declare a structure in Lean to hold implementations of addition.
In the above Lean code, the field add has type
Add.add:{α:Type}→Addα→α→α→α
where the curly braces around the type α mean that it is an implicit argument.
We could implement double by:
Note that you can double a natural number n by double{add:=Nat.add}n.
Of course, it would be highly cumbersome for users to manually pass the
implementations around in this way.
Indeed, it would defeat most of the potential benefits of ad-hoc
polymorphism.
The main idea behind type classes is to make arguments such as Addα implicit,
and to use a database of user-defined instances to synthesize the desired instances
automatically through a process known as typeclass resolution. In Lean, by changing
structure to class in the example above, the type of Add.add becomes:
where the square brackets indicate that the argument of type Addα is instance implicit,
i.e. that it should be synthesized using typeclass resolution. This version of
add is the Lean analogue of the Haskell term add :: Add a => a -> a -> a.
Similarly, we can register instances by:
Then for n : Nat and m : Nat, the term Add.addnm triggers typeclass resolution with
the goal of AddNat, and typeclass resolution will synthesize the instance for Nat above.
We can now reimplement double using an instance implicit by:
In general, instances may depend on other instances in complicated ways. For example,
you can declare an instance stating that if α has addition, then Arrayα
has addition:
Note that (·+·) is notation for funxy=>x+y in Lean.
The example above demonstrates how type classes are used to overload notation.
Now, we explore another application. We often need an arbitrary element of a given type.
Recall that types may not have any elements in Lean.
It often happens that we would like a definition to return an arbitrary element in a “corner case.”
For example, we may like the expression headxs to be of type α when xs is of type Listα.
Similarly, many theorems hold under the additional assumption that a type is not empty.
For example, if α is a type, ∃x:α,x=x is true only if α is not empty.
The standard library defines a type class Inhabited to enable type class inference to infer a
“default” element of an inhabited type.
Let us start with the first step of the program above, declaring an appropriate class:
Note Inhabited.default doesn't have any explicit arguments.
An element of the class Inhabitedα is simply an expression of the form Inhabited.mkx, for some element x : α.
The projection Inhabited.default will allow us to “extract” such an element of α from an element of Inhabitedα.
Now we populate the class with some instances:
If that were the extent of type class inference, it would not be all that impressive;
it would be simply a mechanism of storing a list of instances for the elaborator to find in a lookup table.
What makes type class inference powerful is that one can chain instances. That is,
an instance declaration can in turn depend on an implicit instance of a type class.
This causes class inference to chain through instances recursively, backtracking when necessary, in a Prolog-like search.
For example, the following definition shows that if two types α and β are inhabited, then so is their product:
As an exercise, try defining default instances for other types, such as List and Sum types.
The Lean standard library contains the definition inferInstance. It has type {α:Sortu}→[i:α]→α,
and is useful for triggering the type class resolution procedure when the expected type is an instance.
Definition `foo` of class type is semireducible. Most type class instances should be instance-reducible, so consider marking thisdefinition with `@[instance_reducible]`. If it is intentionally semireducible, this warning can be disabled with `set_option warn.classDefReducibility false`.deffoo:Inhabited(Nat×Nat):=inferInstancetheoremex:foo.default=(default,default):=rfl
You can use the command #print to inspect how simple inferInstance is.
The polymorphic method toString has type {α:Typeu}→[ToStringα]→α→String. You implement the instance
for your own types and use chaining to convert complex values into strings. Lean comes with ToString instances
for most builtin types.
Lean elaborates the terms (2:Nat) and (2:Rational) as
@OfNat.ofNatNat2(@instOfNatNat2) and
@OfNat.ofNatRational2(@instOfNatRational2) respectively.
We say the numerals 2 occurring in the elaborated terms are raw natural numbers.
You can input the raw natural number 2 using the macro nat_lit2.
The OfNat instance is parametric on the numeral. So, you can define instances for particular numerals.
The second argument is often a variable as in the example above, or a raw natural number.
By default, Lean only tries to synthesize an instance InhabitedT when the term T is known and does not
contain missing parts. The following command produces the error
typeclass instance problem is stuck, it is often due to metavariables because the type has a missing part (i.e., the _).
You can view the parameter of the type class Inhabited as an input value for the type class synthesizer.
When a type class has multiple parameters, you can mark some of them as output parameters.
Lean will start type class synthesizer even when these parameters have missing parts.
In the following example, we use output parameters to define a heterogeneous polymorphic
multiplication.
The parameters α and β are considered input parameters and γ an output one.
Given an application hMulab, after the types of a and b are known, the type class
synthesizer is invoked, and the resulting type is obtained from the output parameter γ.
In the example above, we defined two instances. The first one is the homogeneous
multiplication for natural numbers. The second is the scalar multiplication for arrays.
Note that you chain instances and generalize the second instance.
You can use our new scalar array multiplication instance on arrays of type Arrayβ
with a scalar of type α whenever you have an instance HMulαβγ.
In the last #eval, note that the instance was used twice on an array of arrays.
Output parameters are ignored during instance synthesis. Even when instance synthesis occurs in a
context in which the values of output parameters are already determined, their values are ignored.
Once an instance is found using its input parameters, Lean ensures that the already-known values of
the output parameters match those which were found.
Lean also features semi-output parameters, which have some features of input parameters
and some features of output parameters. Like input parameters, semi-output parameters are considered
when selecting instances. Like output parameters, they can be used to instantiate unknown values.
However, they do not do so uniquely. Instance synthesis with semi-output parameters can be more difficult
to predict, because the order in which instances are considered can determine which is selected, but it is
also more flexible.
In the class HMul, the parameters α and β are treated as input values.
Thus, type class synthesis only starts after these two types are known. This may often
be too restrictive.
classHMul(α:Typeu)(β:Typev)(γ:outParam(Typew))wherehMul:α→β→γexportHMul(hMul)instance:HMulIntIntIntwherehMul:=Int.muldefxs:ListInt:=[1,2,3]/--
error: typeclass instance problem is stuck
HMul Int ?m.2 (?m.11 y)
Note: Lean will not try to resolve this typeclass instance problem because the second type argument to `HMul` 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.
-/#guard_msgs(error)in#evalfuny=>xs.map(funx=>hMulxy)
endEx
The instance HMul is not synthesized by Lean because the type of y has not been provided.
However, it is natural to assume that the type of y and x should be the same in
this kind of situation. We can achieve exactly that using default instances.
By tagging the instance above with the attribute [default_instance], we are instructing Lean
to use this instance on pending type class synthesis problems.
The actual Lean implementation defines homogeneous and heterogeneous classes for arithmetical operators.
Moreover, a+b, a*b, a-b, a/b, and a%b are notations for the heterogeneous versions.
The instance OfNatNatn is the default instance (with priority 100) for the OfNat class. This is why the numeral
2 has type Nat when the expected type is not known. You can define default instances with higher
priority to override the builtin ones.
Priorities are also useful to control the interaction between different default instances.
For example, suppose xs has type Listα. When elaborating xs.map(funx=>2*x), we want the homogeneous instance for multiplication
to have higher priority than the default instance for OfNatα2. This is particularly important when we have implemented only the instance
HMulααα, and did not implement HMulNatαα.
Now, we reveal how the notation a * b is defined in Lean.
Type classes are implemented using attributes in Lean. Thus, you can
use the local modifier to indicate that they only have effect until
the current section or namespace is closed, or until the end
of the current file.
structurePointwherex:Naty:Natsectionlocalinstance:AddPointwhereaddab:={x:=a.x+b.x,y:=a.y+b.y}defdouble(p:Point):=p+pend-- instance `Add Point` is not active anymore/--
error: failed to synthesize instance of type class
HAdd Point Point ?m.5
Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/#guard_msgsindeftriple(p:Point):=p+p+p
You can also temporarily disable an instance using the attribute command
until the current section or namespace is closed, or until the end
of the current file.
structurePointwherex:Naty:NatinstanceaddPoint:AddPointwhereaddab:={x:=a.x+b.x,y:=a.y+b.y}defdouble(p:Point):=p+pattribute[-instance]addPoint/--
error: failed to synthesize instance of type class
HAdd Point Point ?m.5
Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/#guard_msgsindeftriple(p:Point):=p+p+p-- Error: failed to synthesize instance
We recommend you only use this command to diagnose problems.
You can also declare scoped instances in namespaces. This kind of instance is
only active when you are inside of the namespace or open the namespace.
structurePointwherex:Naty:NatnamespacePointscopedinstance:AddPointwhereaddab:={x:=a.x+b.x,y:=a.y+b.y}defdouble(p:Point):=p+pendPoint-- instance `Add Point` is not active anymore/--
error: failed to synthesize instance of type class
HAdd Point Point ?m.3
Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/#guard_msgs(error)infunp=>sorry : (p:Point)→?m.6p#checkfun(p:Point)=>p+p+p
funp=>sorry : (p:Point)→?m.6p
namespacePoint-- instance `Add Point` is active againfunp=>p+p+p : Point→Point#checkfun(p:Point)=>p+p+p
Let us consider another example of a type class defined in the
standard library, namely the type class of Decidable
propositions. Roughly speaking, an element of Prop is said to be
decidable if we can decide whether it is true or false. The
distinction is only useful in constructive mathematics; classically,
every proposition is decidable. But if we use the classical principle,
say, to define a function by cases, that function will not be
computable. Algorithmically speaking, the Decidable type class can
be used to infer a procedure that effectively determines whether or
not the proposition is true. As a result, the type class supports such
computational definitions when they are possible while at the same
time allowing a smooth transition to the use of classical definitions
and classical reasoning.
In the standard library, Decidable is defined formally as follows:
Logically speaking, having an element t : Decidablep is stronger
than having an element t':p∨¬p; it enables us to define values
of an arbitrary type depending on the truth value of p. For
example, for the expression ifpthenaelseb to make sense, we
need to know that p is decidable. That expression is syntactic
sugar for itepab, where ite is defined as follows:
That is, in ditecte, we can assume hc : c in the “then”
branch, and hnc : ¬c in the “else” branch. To make dite more
convenient to use, Lean allows us to write ifh:cthentelsee
instead of ditec(funh:c=>th)(funh:¬c=>eh).
Without classical logic, we cannot prove that every proposition is
decidable. But we can prove that certain propositions are
decidable. For example, we can prove the decidability of basic
operations like equality and comparisons on the natural numbers and
the integers. Moreover, decidability is preserved under propositional
connectives:
Turning on implicit arguments shows that the elaborator has inferred
the decidability of the proposition x<a∨x>b, simply by
applying appropriate instances.
With the classical axioms, we can prove that every proposition is
decidable. You can import the classical axioms and make the generic
instance of decidability available by opening the Classical namespace.
Thereafter Decidablep has an instance for every p.
Thus all theorems in the library
that rely on decidability assumptions are freely available when you
want to reason classically. In Axioms and Computation,
we will see that using the law of the
excluded middle to define functions can prevent them from being used
computationally. Thus, the standard library assigns a low priority to
the propDecidable instance.
This guarantees that Lean will favor other instances and fall back on
propDecidable only after other attempts to infer decidability have
failed.
The Decidable type class also provides a bit of small-scale
automation for proving theorems. The standard library introduces the
tactic decide that uses the Decidable instance to solve simple goals,
as well as a function decide that uses a Decidable instance to compute the
corresponding Bool.
They work as follows. The expression decidep tries to infer a
decision procedure for p, and, if it is successful, evaluates to
either true or false. In particular, if p is a true closed
expression, decidep will reduce definitionally to the Boolean true.
On the assumption that decidep=true holds, of_decide_eq_true
produces a proof of p. The tactic decide puts it all together to
prove a target p. By the previous observations,
decide will succeed any time the inferred decision procedure
for p has enough information to evaluate, definitionally, to the isTrue case.
If you are ever in a situation where you need to supply an expression
that Lean can infer by type class inference, you can ask Lean to carry
out the inference using inferInstance:
Definition `foo` of class type is semireducible. Most type class instances should be instance-reducible, so consider marking thisdefinition with `@[instance_reducible]`. If it is intentionally semireducible, this warning can be disabled with `set_option warn.classDefReducibility false`.deffoo:AddNat:=inferInstanceDefinition `bar` of class type is semireducible. Most type class instances should be instance-reducible, so consider marking thisdefinition with `@[instance_reducible]`. If it is intentionally semireducible, this warning can be disabled with `set_option warn.classDefReducibility false`.defbar:Inhabited(Nat→Nat):=inferInstance@inferInstance : {α:Sort u_1}→[i:α]→α#check@inferInstance
Sometimes Lean can't find an instance because the class is buried
under a definition. For example, Lean cannot
find an instance of Inhabited(Setα). We can declare one
explicitly:
defSet(α:Typeu):=α→Prop/--
error: failed to synthesize instance of type class
Inhabited (Set α)
Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
-/#guard_msgsinexample:Inhabited(Setα):=inferInstanceinstance:Inhabited(Setα):=inferInstanceAs(Inhabited(α→Prop))
At times, you may find that the type class inference fails to find an
expected instance, or, worse, falls into an infinite loop and times
out. To help debug in these situations, Lean enables you to request a
trace of the search:
set_optiontrace.Meta.synthInstancetrue
If you are using VS Code, you can read the results by hovering over
the relevant theorem or definition, or opening the messages window
with CtrlShiftEnter.
You can also limit the search using the following options:
Option synthInstance.maxHeartbeats specifies the maximum amount of
heartbeats per typeclass resolution problem. A heartbeat is the number of
(small) memory allocations (in thousands), 0 means there is no limit.
Option synthInstance.maxSize is the maximum number of instances used
to construct a solution in the type class instance synthesis procedure.
Remember also that in both the VS Code and Emacs editor modes, tab
completion works in set_option, to help you find suitable options.
As noted above, the type class instances in a given context represent
a Prolog-like program, which gives rise to a backtracking search. Both
the efficiency of the program and the solutions that are found can
depend on the order in which the system tries the instance. Instances
which are declared last are tried first. Moreover, if instances are
declared in other modules, the order in which they are tried depends
on the order in which namespaces are opened. Instances declared in
namespaces which are opened later are tried earlier.
You can change the order that type class instances are tried by
assigning them a priority. When an instance is declared, it is
assigned a default priority value. You can assign other priorities
when defining an instance. The following example illustrates how this
is done:
The most basic type of coercion maps elements of one type to another. For example, a coercion from Nat to Int allows us to view any element n : Nat as an element of Int. But some coercions depend on parameters; for example, for any type α, we can view any element as : Listα as an element of Setα, namely, the set of elements occurring in the list. The corresponding coercion is defined on the “family” of types Listα, parameterized by α.
Lean allows us to declare three kinds of coercions:
from a family of types to another family of types
from a family of types to the class of sorts
from a family of types to the class of function types
The first kind of coercion allows us to view any element of a member of the source family as an element of a corresponding member of the target family. The second kind of coercion allows us to view any element of a member of the source family as a type. The third kind of coercion allows us to view any element of the source family as a function. Let us consider each of these in turn.
In Lean, coercions are implemented on top of the type class resolution framework. We define a coercion from α to β by declaring an instance of Coeαβ. For example, we can define a coercion from Bool to Prop as follows:
We can define a coercion from Listα to Setα as follows:
defSet(α:Typeu):=α→PropdefSet.empty{α:Typeu}:Setα:=fun_=>FalsedefSet.mem(a:α)(s:Setα):Prop:=sadefSet.singleton(a:α):Setα:=funx=>x=adefSet.union(ab:Setα):Setα:=funx=>ax∨bxnotation"{ "a" }"=>Set.singletonainfix:55" ∪ "=>Set.union
We can use the notation ↑ to force a coercion to be introduced in a particular place. It is also helpful to make our intent clear, and work around limitations of the coercion resolution system.
defSet(α:Typeu):=α→PropdefSet.empty{α:Typeu}:Setα:=fun_=>FalsedefSet.mem(a:α)(s:Setα):Prop:=sadefSet.singleton(a:α):Setα:=funx=>x=adefSet.union(ab:Setα):Setα:=funx=>ax∨bxnotation"{ "a" }"=>Set.singletonainfix:55" ∪ "=>Set.uniondefList.toSet:Listα→Setα|[]=>Set.empty|a::as=>{a}∪as.toSetinstance:Coe(Listα)(Setα)wherecoea:=a.toSet
Lean also supports dependent coercions using the type class CoeDep. For example, we cannot coerce arbitrary propositions to Bool, only the ones that implement the Decidable typeclass.
Lean will also chain (non-dependent) coercions as necessary. Actually, the type class CoeT is the transitive closure of Coe.
Let us now consider the second kind of coercion. By the class of sorts, we mean the collection of universes Typeu. A coercion of the second kind is of the form:
c : (x1 : A1) → ... → (xn : An) → F x1 ... xn → Type u
where F is a family of types as above. This allows us to write s : t whenever t is of type F a₁ ... aₙ. In other words, the coercion allows us to view the elements of F a₁ ... aₙ as types. This is very useful when defining algebraic structures in which one component, the carrier of the structure, is a Type. For example, we can define a semigroup as follows:
In other words, a semigroup consists of a type, carrier, and a multiplication, mul, with the property that the multiplication is associative. The instance command allows us to write a*b instead of Semigroup.mulSab whenever we have ab : S.carrier; notice that Lean can infer the argument S from the types of a and b. The function Semigroup.carrier maps the class Semigroup to the sort Typeu:
It is the coercion that makes it possible to write (abc:S). Note that, we define an instance of CoeSortSemigroup(Typeu) instead of CoeSemigroup(Typeu).
By the class of function types, we mean the collection of Pi types (z:B)→C. The third kind of coercion has the form:
c : (x₁ : A₁) → ... → (xₙ : Aₙ) → (y : F x₁ ... xₙ) → (z : B) → C
where F is again a family of types and B and C can depend on x₁, ..., xₙ, y. This makes it possible to write t s whenever t is an element of F a₁ ... aₙ. In other words, the coercion enables us to view elements of F a₁ ... aₙ as functions. Continuing the example above, we can define the notion of a morphism between semigroups S1 and S2. That is, a function from the carrier of S1 to the carrier of S2 (note the implicit coercion) that respects the multiplication. The projection Morphism.mor takes a morphism to the underlying function:
With the coercion in place, we can write f(a*a*a) instead of f.mor(a*a*a). When the Morphism, f, is used where a function is expected, Lean inserts the coercion. Similar to CoeSort, we have yet another class CoeFun for this class of coercions. The parameter γ is used to specify the function type we are coercing to. This type may depend on the type we are coercing from.