Multiple-inheritance hazards
in dependently-typed
algebraic hierarchies

Eric Wieser
Cambridge University Engineering Department

slides: eric-wieser.github.io/cicm-2023 2023-09-05

Overview

We explore a hazard at the intersection of

  • implementations of multiple
    inheritance of structures
  • dependent types

in the context of algebraic hierarchies.

The slides in this talk show Lean 3 code,
but this is not just about Lean.

Partial Coq and Lean 4 translations are available online at
github.com/eric-wieser/lean-multiple-inheritance

Algebraic structure

Don't want to repeat the same lemma for every type:


                example (a b : ) :
                  a + b = b + a :=
                int.add_comm a b
              

                example (a b : ) :
                  a + b = b + a :=
                rat.add_comm a b
              

                example (a b : ) :
                  a + b = b + a :=
                real.add_comm _ _
              

Solutions include

Typeclasses:

                example {R : Type*} [ring R] (a b : R) :
                  a + b = b + a :=
                add_comm _ _
              
Canonical structures:

                example {R : Ring} (a b : R.carrier) :
                  a + b = b + a :=
                R.add_comm _ _
              
Locales, …

Ring is of course still not general enough…

Algebraic structureMathlib's hierarchy

Or rather, a small subgraph of it

Algebraic structureA simplified hierarchy

Algebraic structureA simplified hierarchy in Lean


              class ring (α) extends
                semiring α, add_comm_group α
            

              class semiring (α) extends
                add_comm_monoid α :=
              (one : α) (mul : α → α → α)
            

              class add_comm_group (α) extends
                add_group α, add_comm_monoid α
            

              class add_comm_monoid (α) extends
                add_monoid α
            

              class add_group (α) extends
                add_monoid α :=
              (neg : α → α)
            

              class add_monoid (α) :=
              (zero : α) (add : α → α → α)
            

(proof fields like add_comm omitted)

extends isn't part of the type theory;
what should it elaborate to?

Multiple inheritance

How can extends be implemented?


            class ring (α) extends
              semiring α, add_comm_group α
          

Naïve


              class ring (α) :=
              (to_semiring : semiring α)
              (to_add_comm_group : add_comm_group α)

              attribute [instance] ring.to_semiring
              attribute [instance] ring.to_add_comm_group
            

Doesn't work because there are two adds!

Multiple inheritance

How can extends be implemented?


            class ring (α) extends
              semiring α, add_comm_group α
          

Flat


                class ring (α) :=
                (zero : α) (add : α → α → α)
                (one : α) (mul : α → α → α)
                (neg : α → α)

                variables (α) [i : ring α]

                instance ring.to_semiring :
                  semiring α := { ..i }

                instance ring.to_add_comm_group :
                  add_comm_group α := { ..i }
              

Nested


                class ring (α) :=
                (to_semiring : semiring α)
                (neg : α → α)
                variables (α) [i : ring α]

                attribute [instance] ring.to_semiring

                instance ring.to_add_comm_group :
                  add_comm_group α :=
                { to_add_group :=
                  { to_add_monoid := sorry, ..i }, ..i }
              

Multiple inheritanceForgetful instances

Flat


                instance ring.to_semiring :
                  semiring α := { ..i }

                instance ring.to_add_comm_group :
                  add_comm_group α := { ..i }
              
ring.mk
zero
zero
add
add
one
one
mul
mul
neg
neg
semiring.mk
zero
zero
add
add
one
one
mul
mul
add_comm_group.mk
zero
zero
add
add
neg
neg

Nested


                attribute [instance] ring.to_semiring

                instance ring.to_add_comm_group :
                  add_comm_group α :=
                { to_add_group :=
                  { to_add_monoid := sorry, ..i }, ..i }
              
ring.mk
to_semiring
.mk
to_add_comm_monoid
.mk
to_add_monoid
.mk
zero
zero
add
add
one
one
mul
mul
neg
neg
semiring.mk
to_add_comm_monoid
.mk
to_add_monoid
.mk
zero
zero
add
add
one
one
mul
mul
add_comm_group.mk
to_add_group
add_group.mk
to_add_monoid
.mk
zero
zero
add
add
neg
neg

Nested avoids unpacking into a new term (orange), which we call preferred (→) …

…but only for some parents;
for others, both give the non-preferred (⇢) path.

Multiple inheritanceA simplified hierarchy

With nested inheritance, paths are either
preferred (→) or non-preferred (⇢)

Every class must have one preferred (→) parent

Many arrangements are possible;
this particular one will be used in this talk.

Multiple inheritanceApproaches in use

Flat


                class ring (α) :=
                (zero : α) (add : α → α → α)
                (one : α) (mul : α → α → α)
                (neg : α → α)
              

Lean 3's old_structure_cmd,
Coq's Hierarchy Builder.

No Lean 4 support.

Nested


                class ring (α) :=
                (to_semiring : semiring α)
                (neg : α → α)
                 
              

Incomplete Lean 3 support.

Full Lean 4 support.

Does the difference matter?

Yes!

source: 1 million lines of mathlib ported
from Lean 3 (flat) to Lean 4 (nested)!

Dependent typeclasses

            
              -- The additive monoid $M$ is an $R$-module
              class module (R M : Type) [semiring R] [add_comm_monoid M] :=
              (smul : R → M → M)
              (one_smul : ∀ (x : M), smul 1 x = x)
              (mul_smul : ∀ (r s : R) (x : M), smul (r * s) x = smul r (smul s x))
              (add_smul : ∀ (r s : R) (x : M), smul (r + s) x = smul r x + smul s x)
              (zero_smul : ∀ (x : M), smul 0 x = 0)
            
            
              -- The additive monoid $M$ is an $R$-module
              class module (R M : Type) [semiring R] [add_comm_monoid M] :=
              (smul : R → M → M)
              (one_smul : ∀ (x : M), smul 1 x = x)
              (mul_smul : ∀ (r s : R) (x : M), smul (r * s) x = smul r (smul s x))
              (add_smul : ∀ (r s : R) (x : M), smul (r + s) x = smul r x + smul s x)
              (zero_smul : ∀ (x : M), smul 0 x = 0)
            
          

Other examples


                -- The semiring $A$ is an $R$-algebra
                class algebra (R A : Type) [comm_semiring R] [semiring A]
              

                  -- There is a $\star$ operator compatible with the existing ring structure on $R$
                  class star_ring (R : Type) [non_unital_semiring R]
                

                  -- The existing norm, $\star$, and ring structure are suitable to declare $R$ a $C^\star$-ring
                  class cstar_ring (R : Type) [non_unital_normed_ring R] [star_ring R]
                

Dependent typeclasses


                class module (R M : Type) [semiring R] [add_comm_monoid M] := (smul : R → M → M)
              

                instance semiring.to_module (R) [iS : semiring R] :
                  module R R :=
                { smul := semiring.mul }
              

                lemma neg_smul {R M} [iR : ring R] [iG : add_comm_group M]
                  [module R M]
                  (r : R) (m : M) :
                  module.smul (-r) m = -(module.smul r m) := sorry
              

                example {R} [iR : ring R] (r : R) (r' : R) :
                  module.smul (-r) r' = -(module.smul r r') :=
                neg_smul r r'error: failed to synthesize type class instance for
                ⊢ module R R
                
              

Everything works correctly with flat inheritance

… but this is not so with nested inheritance!

Dependent typeclasses


                class module (R M : Type) [semiring R] [add_comm_monoid M] := (smul : R → M → M)
              

                instance semiring.to_module (R) [iS : semiring R] :
                  module R R :=
                { smul := semiring.mul }
              

                lemma neg_smul {R M} [iR : ring R] [iG : add_comm_group M]
                  [module R M]
                  (r : R) (m : M) :
                  module.smul (-r) m = -(module.smul r m) := sorry
              

                example {R} [iR : ring R] (r : R) (r' : R) :
                  module.smul (-r) r' = -(module.smul r r') :=
                by haveI : module R R := semiring.to_module R; exact
                neg_smul r r'error: failed to synthesize type class instance for
                _inst : module R Rmodule R R
                
              

Telling lean where the instance is doesn't help!

Dependent typeclasses


                class module (R M : Type) [semiring R] [add_comm_monoid M] := (smul : R → M → M)
              

                instance semiring.to_module (R) [iS : semiring R] :
                  @module R R _ _ :=
                { smul := semiring.mul }
              

                lemma neg_smul {R M} [iR : ring R] [iG : add_comm_group M]
                  [@module R M _ _]
                  (r : R) (m : M) :
                  module.smul (-r) m = -(module.smul r m) := sorry
              

                example {R} [iR : ring R] (r : R) (r' : R) :
                  module.smul (-r) r' = -(module.smul r r') :=
                by haveI : module R R := semiring.to_module R; exact
                neg_smul r r'error: failed to synthesize type class instance for
                _inst : @module R R _ _
                @module R R _ _
                
              

module R R isn't showing us the full picture…

Dependent typeclasses


            class module (R M : Type) [semiring R] [add_comm_monoid M] := (smul : R → M → M)
          

            instance semiring.to_module (R) [iS : semiring R] :
              @module R R iS (@semiring.to_add_comm_monoid R iS) :=
            { smul := semiring.mul }
          

            lemma neg_smul {R M} [iR : ring R] [iG : add_comm_group M]
              [@module R M @ring.to_semiring R iR (@add_comm_group.to_add_comm_monoid M iG)]
              (r : R) (m : M) :
              module.smul (-r) m = -(module.smul r m) := sorry
          

            example {R} [iR : ring R] (r : R) (r' : R) :
              module.smul (-r) r' = -(module.smul r r') :=
            by haveI : module R R := semiring.to_module R; exact
            neg_smul r r'error: failed to synthesize type class instance for
            _inst : @module R R
                (@ring.to_semiring R iR)
                (@semiring.to_add_comm_monoid R (@ring.to_semiring R iR))
            ⊢ @module R R
                (@ring.to_semiring R iR)
                (@add_comm_group.to_add_comm_monoid R (@ring.to_add_comm_group R iR))
            
          

Dependent typeclassesEquating types

Instance


                  @module R R iS (@semiring.to_add_comm_monoid R iS)
                

Set iS = @ring.to_semiring R iR to get


                _inst : @module R R
                    (@ring.to_semiring R iR)
                    (@semiring.to_add_comm_monoid R (@ring.to_semiring R iR))
              

Lemma


                  @module R M @ring.to_semiring R iR (@add_comm_group.to_add_comm_monoid M iG)
                

iS = @ring.to_semiring R iR, iG = @ring.to_add_comm_group R iR


                ⊢ @module R R
                    (@ring.to_semiring R iR)
                    (@add_comm_group.to_add_comm_monoid R (@ring.to_add_comm_group R iR))
              

The semiring R terms are (definitionally) equal

Are the add_comm_monoid R terms?

Dependent typeclassesEquating types

Instance


                _inst : @module R R
                    (@ring.to_semiring R iR)
                    (@semiring.to_add_comm_monoid R (@ring.to_semiring R iR))
              

Lemma


                ⊢ @module R R
                    (@ring.to_semiring R iR)
                    (@add_comm_group.to_add_comm_monoid R (@ring.to_add_comm_group R iR))
              

The semiring R terms are (definitionally) equal

Are the add_comm_monoid R terms?


            example :
              @semiring.to_add_comm_monoid R
              (@ring.to_semiring R iR)
                  =
              @add_comm_group.to_add_comm_monoid R
              (@ring.to_add_comm_group R iR) :=
            rfl
          

Not with nested inheritance in Lean 3 (or Coq)!

Dependent typeclassesFlat structures work

ring
semiring
add_comm_group
add_comm_monoid
ring.mk
zero
zero
add
add
one
one
mul
mul
neg
neg
semiring.mk
zero
zero
add
add
one
one
mul
mul

                  @semiring.to_add_comm_monoid R
                    (@ring.to_semiring R iR)
                

                  @add_comm_group.to_add_comm_monoid R
                    (@ring.to_add_comm_group R iR)
                
add_comm_monoid.mk
zero
zero
add
add
add_comm_group.mk
zero
zero
add
add
neg
neg
add_comm_monoid.mk
zero
zero
add
add
$\stackrel{\mathclap{\mathrm{def.}}}{=}$

Dependent typeclassesNested structures don't work

ring
semiring
add_comm_group
add_comm_monoid
ring.mk
to_semiring
.mk
to_add_comm_monoid
.mk
to_add_monoid
.mk
zero
zero
add
add
one
one
mul
mul
neg
neg
semiring.mk
to_add_comm_monoid
.mk
to_add_monoid
.mk
zero
zero
add
add
one
one
mul
mul

                  @semiring.to_add_comm_monoid R
                    (@ring.to_semiring R iR)
                

                  @add_comm_group.to_add_comm_monoid R
                    (@ring.to_add_comm_group R iR)
                
add_comm_monoid.mk
to_add_monoid
.mk
zero
zero
add
add
add_comm_group.mk
to_add_group
add_group.mk
to_add_monoid
.mk
zero
zero
add
add
neg
neg
add_comm_monoid.mk
to_add_monoid
.mk
zero
zero
add
add
$\stackrel{\mathclap{\mathrm{def.}}}{\ne}$

Dependent typeclassesThe problem

add_comm_monoid.mk
to_add_monoid
.mk
zero
zero
add
add
$\stackrel{\mathclap{\mathrm{def.}}}{\ne}$
add_comm_monoid.mk
to_add_monoid
.mk
zero
zero
add
add

?

add_comm_monoid.mk iACM.to_add_monoid

iACM : add_comm_monoid R

This cannot be solved by

… including poorer structures into richer ones; this way, “deducing” one structure from the other always amounts to erasure of data

Competing Inheritance Paths in Dependent Type Theory: A Case Study in Functional Analysis Reynald Affeldt, Cyril Cohen, Marie Kerjean, Assia Mahboubi, Damien Rouhling, Kazuhiko Sakaguchi

(as add and zero already agree!)

$\eta$-reduction of structures

Let's switch to a type theory where

add_comm_monoid.mk
to_add_monoid
.mk
zero
zero
add
add
$\stackrel{\mathclap{\mathrm{def.}}}{=}$
add_comm_monoid.mk
to_add_monoid
.mk
zero
zero
add
add

!


          structure point := (x y : ℤ)

          -- fails in Lean 3, succeeds in Lean 4
          example (p : point) : p = { x := p.x, y := p.y } := rfl
          example (p : point) : p = point.mk p.x p.y := rfl
          

Other advantages

Nice equalities:


                example (e : A ≃ B) :
                  e.symm.symm = e := rfl
                

fixes diamonds of the form:


                example (S : Subalgebra A A) (s : S) :
                  ⟨algebraMap S S s.val, _⟩ = algebraMap S S s := rfl
                

$\eta$-reduction of structuresLanguage support

Coq * Can be enabled with Primitive Projections.
Hierarchy Builder uses flat inheritance so does not need this.
Agda *Can be disabled with no-eta-equality
Lean 3 nested inheritance used rarely in practice
Lean 4
(— Feb 2023)
* Not during typeclass search
(Feb — May 2023) * … unless using synthInstance.etaExperiment
(May 2023 —)

Workarounds

What if we're stuck in a type theory
without $\eta$ for structures?

WorkaroundsEmulating flat structures

If the language supports flat inheritance, use it.

(recall: ⇢ indicates paths with (some) eta expansion)

If it doesn't, add a hack …

WorkaroundsPlacing paths optimally

Swapping the preferred paths
makes our square commute

Likely possible to do this to any graph,
but not to so by hand to all of mathlib!

WorkaroundsExpanding arguments to module


              class module (R M : Type)
                [semiring R]
                [add_comm_monoid M] :=
              (smul : R → M → M)
            


            class module (R M : Type)
              [has_zero R] [has_add R] [has_one R] [has_mul R]
              [has_zero M] [has_add M] :=
            (smul : R → M → M)
            

The has_* classes are all roots of the typeclass graph, so paths always commute.

Downsides

Much larger term sizes

6 arguments instead of two,
each of which is itself a larger term.

Not always possible

Works in this case because none of the types depend on the proof fields of semiring e.t.c.

Packed structures

Is this just a typeclass problem?


          structure packed_semiring := (carrier : Type) [semiring carrier]
          structure packed_add_comm_monoid := (carrier : Type) [add_comm_monoid carrier]
          

A naïve encoding of a module:


          structure packed_module :=
          (R : packed_semiring) (M : packed_add_comm_monoid) [module R.carrier M.carrier]
          

This type is immune, as it's not dependent!

This type is hard to use, as it's not dependent!


            structure linear_map (V W : packed_module) (hVW : V.R = W.R) : Type :=
            (to_fun : V.M.carrier → W.M.carrier)
            (map_smul : ∀ (r : V.R.carrier) v,
              to_fun (r • v) = cast (congr_arg _ h) r • to_fun v)
            

(We want both modules to be over the same ring)

Packed structures

Is this just a typeclass problem?


          structure packed_semiring        := (carrier : Type) [semiring carrier]
          structure packed_add_comm_monoid := (carrier : Type) [add_comm_monoid carrier]
          

A realistic encoding of a module:


          structure packed_module (R : packed_semiring) :=
          (M : packed_add_comm_monoid M) [module R.carrier M.carrier]
          

          structure linear_map (R : packed_semiring) (V W : packed_module R) :=
          (to_fun : V.M.carrier → W.M.carrier)
          (map_smul : ∀ (r : R.carrier) v, to_fun (r • v) = r • to_fun v)
          

Packed structures

Is this just a typeclass problem?


            structure packed_semiring        := (carrier : Type) [semiring carrier]
            structure packed_add_comm_monoid := (carrier : Type) [add_comm_monoid carrier]
            

A realistic encoding of a module:


            structure packed_module (R : packed_semiring) :=
            (M : packed_add_comm_monoid M) [module R.carrier M.carrier]
            

The $\eta$ problem reappears for the R argument, this time for paths from packed_comm_ring to packed_semiring:


            example (R : packed_comm_ring) :
                  packed_module R.to_packed_ring.to_packed_semiring
                = packed_module R.to_packed_comm_semiring.to_packed_semiring :=
              rfl
            

Packed structures have the same problem

Summary

In practice, nested inheritance is only compatible with dependent types if $\eta$-reduction of structures is supported.

Not typeclass-specific; canonical structures (implemented via packed structures) hit the same problem.

Other systems avoided the issue by chance:

  • Coq's Hierarchy builder used flat structures for simplicity
  • Agda uses $\eta$-reduction by default for convenience
  • Lean 3 never migrated off old_structure_cmd

Structural $\eta$-reduction in Lean 4 was crucial to porting mathlib from Lean 3 to Lean 4.

Code samples in Lean 3, Lean 4, and Coq:
github.com/eric-wieser/lean-multiple-inheritance
These slides: eric-wieser.github.io/cicm-2023

Appendix

The flat approach


                class add_monoid (α : Type) :=
                (zero : α) (add : α → α → α)
              

                class add_comm_monoid (α : Type) :=
                (zero : α) (add : α → α → α)

                instance add_comm_monoid.to_add_monoid
                  (α : Type) [i : add_comm_monoid α] : add_monoid α := { ..i }
              

                class semiring (α : Type) :=
                (zero : α) (add : α → α → α)
                (one : α) (mul : α → α → α)

                instance semiring.to_add_comm_monoid
                  (α : Type) [i : semiring α] : add_comm_monoid α := { ..i }
              

                class add_group (α : Type) :=
                (zero : α) (add : α → α → α)
                (neg : α → α)

                instance add_group.to_add_monoid
                  (α : Type) [i : add_group α] : add_monoid α := { ..i }
              

                class add_comm_group (α : Type) :=
                (zero : α) (add : α → α → α) (neg : α → α)

                instance add_comm_group.to_add_group
                  (α : Type) [i : add_comm_group α] : add_group α := { ..i }

                instance add_comm_group.to_add_comm_monoid
                  (α : Type) [i : add_comm_group α] : add_comm_monoid α := { ..i }
              

                class ring (α : Type) :=
                (zero : α) (add : α → α → α) (neg : α → α)

                instance ring.to_semiring
                  (α : Type) [i : ring α] : semiring α := { ..i }

                instance ring.to_add_comm_group
                  (α : Type) [i : ring α] : add_comm_group α := { ..i }




                
              

The nested approach


                class add_monoid (α : Type) :=
                (zero : α) (add : α → α → α)
              

                class add_comm_monoid (α : Type) :=
                (to_add_monoid : add_monoid α)

                attribute [instance] add_comm_monoid.to_add_monoid

                
              

                class semiring (α : Type) :=
                (to_add_comm_monoid : add_comm_monoid α)
                (one : α) (mul : α → α → α)

                attribute [instance] semiring.to_add_comm_monoid

                
              

                class add_group (α : Type) :=
                (to_add_monoid : add_monoid α)
                (neg : α → α)

                attribute [instance] add_group.to_add_monoid

                
              

                class add_comm_group (α : Type) :=
                (to_add_group : add_group α)

                attribute [instance] add_comm_group.to_add_group

                @[priority 100] instance add_comm_group.to_add_comm_monoid
                  {α : Type} [i : add_comm_group α] : add_comm_monoid α :=
                { to_add_monoid := i.to_add_group.to_add_monoid, ..i }
              

                class ring (α : Type) :=
                (to_semiring : semiring α)
                (neg : α → α)

                attribute [instance] ring.to_semiring

                @[priority 100] instance ring.to_add_comm_group
                  (α : Type) [i : ring α] : add_comm_group α :=
                { to_add_group :=
                  { to_add_monoid :=
                      i.to_semiring.to_add_comm_monoid.to_add_monoid, ..i },
                  .. i }