Coherence Tactics

3.2. Implementation🔗

The previous chapter explained monoidal_coherence and bicategory_coherence, which solve goals built only from associators and unitors. This chapter explains what is added by monoidal and bicategory when a goal contains actual maps, not merely those constructed from associators and unitors. The extra work is to rearrange each side until those maps can be compared directly, leaving only coherence equalities for the previous chapter.

3.2.1. Shared Core🔗

The previous chapter explained the design of the shared core. What is added here is the normalization of 2-morphisms.

The core recursive function is eval, which rearranges the parsed expression into a form where the actual maps can be compared one by one. The classes MkEvalComp, MkEvalWhiskerLeft, MkEvalWhiskerRight, MkEvalHorizontalComp, and MkEval support this function by supplying the equality proofs needed at each recursive step.

Normalization means separating two kinds of data in the composite. The actual maps stay in the same order. Associators and unitors are pushed into the coherence pieces around them. The target shape is an alternating composite

\alpha_0 \,\mathbin{\gg}\, \eta_1 \,\mathbin{\gg}\, \alpha_1 \,\mathbin{\gg}\, \cdots \,\mathbin{\gg}\, \eta_n \,\mathbin{\gg}\, \alpha_n,

where each \alpha_i is one coherence piece built only from associators and unitors. More precisely, the source code defines each \eta_i to have the form

f_1 \triangleleft \cdots \triangleleft f_n \triangleleft \theta.

Here \theta has the form

\iota_1 \otimes \cdots \otimes \iota_\ell,

and each \iota_j has the form

\kappa \triangleright g_1 \triangleright \cdots \triangleright g_k,

where \kappa is one atomic 2-morphism from the original expression. In the implementation, these three layers are represented by WhiskerLeft, HorizontalComp, and WhiskerRight. The datatype NormalExpr then stores the full alternating expression.

/-- Expressions of the form `η ▷ f₁ ▷ ... ▷ fₙ`. -/ inductive WhiskerRight : Type /-- Construct the expression for an atomic 2-morphism. -/ | of (η : Atom) : WhiskerRight /-- Construct the expression for `η ▷ f`. -/ | whisker (e : Mor₂) (η : WhiskerRight) (f : Atom₁) : WhiskerRight deriving Inhabited/-- Expressions of the form `η₁ ⊗ ... ⊗ ηₙ`. -/ inductive HorizontalComp : Type | of (η : WhiskerRight) : HorizontalComp | cons (e : Mor₂) (η : WhiskerRight) (ηs : HorizontalComp) : HorizontalComp deriving Inhabited/-- Expressions of the form `f₁ ◁ ... ◁ fₙ ◁ η`. -/ inductive WhiskerLeft : Type /-- Construct the expression for a right-whiskered 2-morphism. -/ | of (η : HorizontalComp) : WhiskerLeft /-- Construct the expression for `f ◁ η`. -/ | whisker (e : Mor₂) (f : Atom₁) (η : WhiskerLeft) : WhiskerLeft deriving Inhabited/-- Normalized expressions for 2-morphisms. -/ inductive NormalExpr : Type /-- Construct the expression for a structural 2-morphism. -/ | nil (e : Mor₂) (α : Structural) : NormalExpr /-- Construct the normalized expression of a 2-morphism `α ≫ η ≫ ηs` recursively. -/ | cons (e : Mor₂) (α : Structural) (η : WhiskerLeft) (ηs : NormalExpr) : NormalExpr deriving Inhabited

The result type Eval.Result packages such a normalized expression together with a proof that it is equal to the original term.

/-- The result of evaluating an expression into normal form. -/ structure Eval.Result where /-- The normalized expression of the 2-morphism. -/ expr : NormalExpr /-- The proof that the normalized expression is equal to the original expression. -/ proof : Expr deriving Inhabited

The function that actually carries out this normalization is eval.

-- TODO: It takes a while to compile. Find out why. /-- Evaluate the expression of a 2-morphism into a normalized form. -/ def eval (nm : Name) (e : Mor₂) : CoherenceM ρ Eval.Result := do withTraceNode nm (fun _ => return m!"eval: {e.e}") do match e with | .isoHom _ _ α => withTraceNode nm (fun _ => return m!"Iso.hom") do match α with | .structuralAtom α => return nilM <| .structuralAtom α, mkEqRefl e.e | .of η => let η MonadMor₂.atomHomM η let result mkEvalOf η traceProof nm result return NormalExpr.ofAtomM η, result | _ => throwError "not implemented. try dsimp first." | .isoInv _ _ α => withTraceNode nm (fun _ => return m!"Iso.inv") do match α with | .structuralAtom α => return nilM <| ( symmM (.structuralAtom α)), mkEqRefl e.e | .of η => let η MonadMor₂.atomInvM η let result mkEvalOf η traceProof nm result return NormalExpr.ofAtomM η, result | _ => throwError "not implemented. try dsimp first." | .id _ _ f => let α MonadMor₂Iso.id₂M f return nilM <| .structuralAtom α, mkEqRefl e.e | .comp _ _ _ _ _ η θ => withTraceNode nm (fun _ => return m!"comp") do let η', e_η eval nm η let θ', e_θ eval nm θ let ηθ, pf evalComp η' θ' let result mkEvalComp η θ η' θ' ηθ e_η e_θ pf traceProof nm result return ηθ, result | .whiskerLeft _ _ f _ _ η => withTraceNode nm (fun _ => return m!"whiskerLeft") do let η', e_η eval nm η let θ, e_θ evalWhiskerLeft f η' let result mkEvalWhiskerLeft f η η' θ e_η e_θ traceProof nm result return θ, result | .whiskerRight _ _ _ _ η h => withTraceNode nm (fun _ => return m!"whiskerRight") do let η', e_η eval nm η let θ, e_θ evalWhiskerRight η' h let result mkEvalWhiskerRight η h η' θ e_η e_θ traceProof nm result return θ, result | .coherenceComp _ _ _ _ _ _ α₀ η θ => withTraceNode nm (fun _ => return m!"monoidalComp") do let η', e_η eval nm η let α₀ := .structuralAtom <| .coherenceHom α₀ let α nilM α₀ let θ', e_θ eval nm θ let αθ, e_αθ evalComp α θ' let ηαθ, e_ηαθ evalComp η' αθ let result mkEvalMonoidalComp η θ α₀ η' θ' αθ ηαθ e_η e_θ e_αθ e_ηαθ traceProof nm result return ηαθ, result | .horizontalComp _ _ _ _ _ _ η θ => withTraceNode nm (fun _ => return m!"horizontalComp") do let η', e_η eval nm η let θ', e_θ eval nm θ let ηθ, e_ηθ evalHorizontalComp η' θ' let result mkEvalHorizontalComp η θ η' θ' ηθ e_η e_θ e_ηθ traceProof nm result return ηθ, result | .of η => let result mkEvalOf η traceProof nm result return NormalExpr.ofAtomM η, result

The helper evalComp combines two already-normalized expressions.

/-- Evaluate the expression `η ≫ θ` into a normalized form. -/ def evalComp : NormalExpr NormalExpr CoherenceM ρ Eval.Result | .nil _ α, η => do evalCompNil α η | .cons _ α η ηs, θ => do let ι, e_ι evalComp ηs θ return consM α η ι, mkEvalCompCons α η ηs θ ι e_ι

The helper evalWhiskerLeft handles left whiskering.

/-- Evaluate the expression `f ◁ η` into a normalized form. -/ def evalWhiskerLeft : Mor₁ NormalExpr CoherenceM ρ Eval.Result | f, .nil _ α => do return nilM ( whiskerLeftM f α), mkEvalWhiskerLeftNil f α | .of f, .cons _ α η ηs => do let η' MonadWhiskerLeft.whiskerLeftM f η let θ, e_θ evalWhiskerLeft (.of f) ηs let η'' consM ( whiskerLeftM (.of f) α) η' θ return η'', mkEvalWhiskerLeftOfCons f α η ηs θ e_θ | .comp _ f g, η => do let θ, e_θ evalWhiskerLeft g η let ι, e_ι evalWhiskerLeft f θ let h η.srcM let h' η.tgtM let ι', e_ι' evalComp ι ( NormalExpr.associatorInvM f g h') let ι'', e_ι'' evalComp ( NormalExpr.associatorM f g h) ι' return ι'', mkEvalWhiskerLeftComp f g η θ ι ι' ι'' e_θ e_ι e_ι' e_ι'' | .id _ _, η => do let f η.srcM let g η.tgtM let η', e_η' evalComp η ( NormalExpr.leftUnitorInvM g) let η'', e_η'' evalComp ( NormalExpr.leftUnitorM f) η' return η'', mkEvalWhiskerLeftId η η' η'' e_η' e_η''

The helper evalWhiskerRight for right whiskering and evalHorizontalComp for horizontal composition are more involved. One reason is that the normal form stores actual maps as WhiskerLeft terms, and the expression in the form (f ◁ η) ▷ h cannot be in the normal form. These two helper functions are implemented by mutual recursion with auxiliary functions.

mutual /-- Evaluate the expression `η ▷ f` into a normalized form. -/ partial def evalWhiskerRightAux : HorizontalComp Atom₁ CoherenceM ρ Eval.Result | .of η, f => do let η' NormalExpr.ofM <| .of <| .of <| MonadWhiskerRight.whiskerRightM η f return η', mkEvalWhiskerRightAuxOf η f | .cons _ η ηs, f => do let ηs', e_ηs' evalWhiskerRightAux ηs f let η₁, e_η₁ evalHorizontalComp ( NormalExpr.ofM <| .of <| .of η) ηs' let η₂, e_η₂ evalComp η₁ ( NormalExpr.associatorInvM ( η.tgtM) ( ηs.tgtM) (.of f)) let η₃, e_η₃ evalComp ( NormalExpr.associatorM ( η.srcM) ( ηs.srcM) (.of f)) η₂ return η₃, mkEvalWhiskerRightAuxCons f η ηs ηs' η₁ η₂ η₃ e_ηs' e_η₁ e_η₂ e_η₃ /-- Evaluate the expression `η ▷ f` into a normalized form. -/ partial def evalWhiskerRight : NormalExpr Mor₁ CoherenceM ρ Eval.Result | .nil _ α, h => do return nilM ( whiskerRightM α h), mkEvalWhiskerRightNil α h | .cons _ α (.of η) ηs, .of f => do let ηs₁, e_ηs₁ evalWhiskerRight ηs (.of f) let η₁, e_η₁ evalWhiskerRightAux η f let η₂, e_η₂ evalComp η₁ ηs₁ let η₃, e_η₃ evalCompNil ( whiskerRightM α (.of f)) η₂ return η₃, mkEvalWhiskerRightConsOfOf f α η ηs ηs₁ η₁ η₂ η₃ e_ηs₁ e_η₁ e_η₂ e_η₃ | .cons _ α (.whisker _ f η) ηs, h => do let g η.srcM let g' η.tgtM let η₁, e_η₁ evalWhiskerRight ( consM ( id₂M' g) η ( NormalExpr.idM g')) h let η₂, e_η₂ evalWhiskerLeft (.of f) η₁ let ηs₁, e_ηs₁ evalWhiskerRight ηs h let α' whiskerRightM α h let ηs₂, e_ηs₂ evalComp ( NormalExpr.associatorInvM (.of f) g' h) ηs₁ let η₃, e_η₃ evalComp η₂ ηs₂ let η₄, e_η₄ evalComp ( NormalExpr.associatorM (.of f) g h) η₃ let η₅, e_η₅ evalComp ( nilM α') η₄ return η₅, mkEvalWhiskerRightConsWhisker f h α η ηs η₁ η₂ ηs₁ ηs₂ η₃ η₄ η₅ e_η₁ e_η₂ e_ηs₁ e_ηs₂ e_η₃ e_η₄ e_η₅ | η, .comp _ g h => do let η₁, e_η₁ evalWhiskerRight η g let η₂, e_η₂ evalWhiskerRight η₁ h let f η.srcM let f' η.tgtM let η₃, e_η₃ evalComp η₂ ( NormalExpr.associatorM f' g h) let η₄, e_η₄ evalComp ( NormalExpr.associatorInvM f g h) η₃ return η₄, mkEvalWhiskerRightComp g h η η₁ η₂ η₃ η₄ e_η₁ e_η₂ e_η₃ e_η₄ | η, .id _ _ => do let f η.srcM let g η.tgtM let η₁, e_η₁ evalComp η ( NormalExpr.rightUnitorInvM g) let η₂, e_η₂ evalComp ( NormalExpr.rightUnitorM f) η₁ return η₂, mkEvalWhiskerRightId η η₁ η₂ e_η₁ e_η₂ /-- Evaluate the expression `η ⊗ θ` into a normalized form. -/ partial def evalHorizontalCompAux : HorizontalComp HorizontalComp CoherenceM ρ Eval.Result | .of η, θ => do return NormalExpr.ofM <| .of <| MonadHorizontalComp.hConsM η θ, mkEvalHorizontalCompAuxOf η θ | .cons _ η ηs, θ => do let α NormalExpr.associatorM ( η.srcM) ( ηs.srcM) ( θ.srcM) let α' NormalExpr.associatorInvM ( η.tgtM) ( ηs.tgtM) ( θ.tgtM) let ηθ, e_ηθ evalHorizontalCompAux ηs θ let η₁, e_η₁ evalHorizontalComp ( NormalExpr.ofM <| .of <| .of η) ηθ let ηθ₁, e_ηθ₁ evalComp η₁ α' let ηθ₂, e_ηθ₂ evalComp α ηθ₁ return ηθ₂, mkEvalHorizontalCompAuxCons η ηs θ ηθ η₁ ηθ₁ ηθ₂ e_ηθ e_η₁ e_ηθ₁ e_ηθ₂ /-- Evaluate the expression `η ⊗ θ` into a normalized form. -/ partial def evalHorizontalCompAux' : WhiskerLeft WhiskerLeft CoherenceM ρ Eval.Result | .of η, .of θ => evalHorizontalCompAux η θ | .whisker _ f η, θ => do let ηθ, e_ηθ evalHorizontalCompAux' η θ let ηθ₁, e_ηθ₁ evalWhiskerLeft (.of f) ηθ let ηθ₂, e_ηθ₂ evalComp ηθ₁ ( NormalExpr.associatorInvM (.of f) ( η.tgtM) ( θ.tgtM)) let ηθ₃, e_ηθ₃ evalComp ( NormalExpr.associatorM (.of f) ( η.srcM) ( θ.srcM)) ηθ₂ return ηθ₃, mkEvalHorizontalCompAux'Whisker f η θ ηθ ηθ₁ ηθ₂ ηθ₃ e_ηθ e_ηθ₁ e_ηθ₂ e_ηθ₃ | .of η, .whisker _ f θ => do let η₁, e_η₁ evalWhiskerRightAux η f let ηθ, e_ηθ evalHorizontalComp η₁ ( NormalExpr.ofM θ) let ηθ₁, e_ηθ₁ evalComp ηθ ( NormalExpr.associatorM ( η.tgtM) (.of f) ( θ.tgtM)) let ηθ₂, e_ηθ₂ evalComp ( NormalExpr.associatorInvM ( η.srcM) (.of f) ( θ.srcM)) ηθ₁ return ηθ₂, mkEvalHorizontalCompAux'OfWhisker f η θ ηθ η₁ ηθ₁ ηθ₂ e_η₁ e_ηθ e_ηθ₁ e_ηθ₂ /-- Evaluate the expression `η ⊗ θ` into a normalized form. -/ partial def evalHorizontalComp : NormalExpr NormalExpr CoherenceM ρ Eval.Result | .nil _ α, .nil _ β => do return nilM <| horizontalCompM α β, mkEvalHorizontalCompNilNil α β | .nil _ α, .cons _ β η ηs => do let η₁, e_η₁ evalWhiskerLeft ( α.tgtM) ( NormalExpr.ofM η) let ηs₁, e_ηs₁ evalWhiskerLeft ( α.tgtM) ηs let η₂, e_η₂ evalComp η₁ ηs₁ let η₃, e_η₃ evalCompNil ( horizontalCompM α β) η₂ return η₃, mkEvalHorizontalCompNilCons α β η ηs η₁ ηs₁ η₂ η₃ e_η₁ e_ηs₁ e_η₂ e_η₃ | .cons _ α η ηs, .nil _ β => do let η₁, e_η₁ evalWhiskerRight ( NormalExpr.ofM η) ( β.tgtM) let ηs₁, e_ηs₁ evalWhiskerRight ηs ( β.tgtM) let η₂, e_η₂ evalComp η₁ ηs₁ let η₃, e_η₃ evalCompNil ( horizontalCompM α β) η₂ return η₃, mkEvalHorizontalCompConsNil α β η ηs η₁ ηs₁ η₂ η₃ e_η₁ e_ηs₁ e_η₂ e_η₃ | .cons _ α η ηs, .cons _ β θ θs => do let ηθ, e_ηθ evalHorizontalCompAux' η θ let ηθs, e_ηθs evalHorizontalComp ηs θs let ηθ₁, e_ηθ₁ evalComp ηθ ηθs let ηθ₂, e_ηθ₂ evalCompNil ( horizontalCompM α β) ηθ₁ return ηθ₂, mkEvalHorizontalCompConsCons α β η θ ηs θs ηθ ηθs ηθ₁ ηθ₂ e_ηθ e_ηθs e_ηθ₁ e_ηθ₂ end

Using eval, the function normalForm replace the original goal by the equality of the two normalized expressions.

/-- Transform an equality between 2-morphisms into the equality between their normalizations. -/ def normalForm (ρ : Type) [Context ρ] [MonadMor₁ (CoherenceM ρ)] [MonadMor₂Iso (CoherenceM ρ)] [MonadNormalExpr (CoherenceM ρ)] [MkEval (CoherenceM ρ)] [MkMor₂ (CoherenceM ρ)] [MonadMor₂ (CoherenceM ρ)] (nm : Name) (mvarId : MVarId) : MetaM (List MVarId) := do mvarId.withContext do let e instantiateMVars <| mvarId.getType withTraceNode nm (fun _ => return m!"normalize: {e}") do let some (_, e₁, e₂) := ( whnfR <| instantiateMVars <| e).eq? | throwError "{nm}_nf requires an equality goal" let ctx : ρ mkContext e₁ CoherenceM.run (ctx := ctx) do let e₁' MkMor₂.ofExpr e₁ let e₂' MkMor₂.ofExpr e₂ let e₁'' eval nm e₁' let e₂'' eval nm e₂' let H mkAppM ``mk_eq #[e₁, e₂, e₁''.expr.e.e, e₂''.expr.e.e, e₁''.proof, e₂''.proof] mvarId.apply H

Then ofNormalizedEq examines an equality of normalized composites and decomposes the leading cons layer. Its theorem mk_eq_of_cons turns α ≫ η ≫ ηs = α' ≫ η' ≫ ηs' into three new goals: the first coherence piece, the next actual map, and the remaining tail.

/-- Split the goal `α ≫ η ≫ ηs = α' ≫ η' ≫ ηs'` into `α = α'`, `η = η'`, and `ηs = ηs'`. -/ def ofNormalizedEq (mvarId : MVarId) : MetaM (List MVarId) := do mvarId.withContext do let e instantiateMVars <| mvarId.getType let some (_, e₁, e₂) := ( whnfR e).eq? | throwError "requires an equality goal" match ( whnfR e₁).getAppFnArgs, ( whnfR e₂).getAppFnArgs with | (``CategoryStruct.comp, #[_, _, _, _, _, α, η]), (``CategoryStruct.comp, #[_, _, _, _, _, α', η']) => match ( whnfR η).getAppFnArgs, ( whnfR η').getAppFnArgs with | (``CategoryStruct.comp, #[_, _, _, _, _, η, ηs]), (``CategoryStruct.comp, #[_, _, _, _, _, η', ηs']) => let e_α mkFreshExprMVar ( Meta.mkEq α α') let e_η mkFreshExprMVar ( Meta.mkEq η η') let e_ηs mkFreshExprMVar ( Meta.mkEq ηs ηs') let x mvarId.apply ( mkAppM ``mk_eq_of_cons #[α, α', η, η', ηs, ηs', e_α, e_η, e_ηs]) return x | _, _ => throwError "failed to make a normalized equality for {e}" | _, _ => throwError "failed to make a normalized equality for {e}"

Finally, main first calls normalForm to replace the original goal by an equality of normalized expressions. It then repeats ofNormalizedEq until no further cons layer remains.

At that point the remaining goals alternate between coherence pieces and actual maps. The actual-map goals are closed by reflexivity. The remaining coherence goals are then sent to the pure coherence tactic from the previous chapter.

/-- The core function for `monoidal` and `bicategory` tactics. -/ def main (ρ : Type) [Context ρ] [MonadMor₁ (CoherenceM ρ)] [MonadMor₂Iso (CoherenceM ρ)] [MonadNormalExpr (CoherenceM ρ)] [MkEval (CoherenceM ρ)] [MkMor₂ (CoherenceM ρ)] [MonadMor₂ (CoherenceM ρ)] [MonadCoherehnceHom (CoherenceM ρ)] [MonadNormalizeNaturality (CoherenceM ρ)] [MkEqOfNaturality (CoherenceM ρ)] (nm : Name) (mvarId : MVarId) : MetaM (List MVarId) := mvarId.withContext do let mvarIds normalForm ρ nm mvarId let (mvarIdsCoherence, mvarIdsRefl) := List.splitEvenOdd ( repeat' ofNormalizedEq mvarIds) for mvarId in mvarIdsRefl do mvarId.refl let mvarIds'' mvarIdsCoherence.mapM fun mvarId => do withTraceNode nm (fun _ => do return m!"goal: { mvarId.getType}") do try pureCoherence ρ nm mvarId catch _ => return [mvarId] return mvarIds''.flatten

3.2.2. Monoidal Front End🔗

The monoidal front end is the code around the user-facing tactic

/-- Use the coherence theorem for monoidal categories to solve equations in a monoidal category, where the two sides only differ by replacing strings of monoidal structural morphisms (that is, associators, unitors, and identities) with different strings of structural morphisms with the same source and target. That is, `monoidal` can handle goals of the form `a ≫ f ≫ b ≫ g ≫ c = a' ≫ f ≫ b' ≫ g ≫ c'` where `a = a'`, `b = b'`, and `c = c'` can be proved using `monoidal_coherence`. -/ def monoidal (mvarId : MVarId) : MetaM (List MVarId) := BicategoryLike.main Monoidal.Context `monoidal mvarId

3.2.3. Bicategorical Front End🔗

The bicategorical front end is the code around the user-facing tactic

/-- Use the coherence theorem for bicategories to solve equations in a bicategory, where the two sides only differ by replacing strings of bicategory structural morphisms (that is, associators, unitors, and identities) with different strings of structural morphisms with the same source and target. That is, `bicategory` can handle goals of the form `a ≫ f ≫ b ≫ g ≫ c = a' ≫ f ≫ b' ≫ g ≫ c'` where `a = a'`, `b = b'`, and `c = c'` can be proved using `bicategory_coherence`. -/ def bicategory (mvarId : MVarId) : MetaM (List MVarId) := BicategoryLike.main Bicategory.Context `bicategory mvarId