# Monoidal categories in lambeq¶

In order to use the advanced features of `lambeq`

and extend it, an understanding of monoidal categories and how it is implemented in the `lambeq.backend`

is required.

## Categories¶

A *category* consists of a collection of *objects* \(A, B, C, \ldots\) and a collection of *morphisms* between objects of the form \(f: A \to B, g: B \to C, h: C \to D, \ldots\), such that:

Morphisms with matching types compose. For example, \(f: A \to B\) and \(g: B \to C\) can compose to make \(g \circ f: A \to C\), but not \(f \circ g\).

Morphisms compose in an associative way: \((h \circ g) \circ f = h \circ (g \circ f)\)

Each object has an identity arrow: \(1_B \circ f = f = f \circ 1_A\)

These definitions are implicitly encoded in this *commutative diagram*: any directed path between two specific objects represents equal morphisms.

For *free* categories: we first define generating objects with the `Ty`

class and generating morphisms with the `Box`

class, then build composite morphisms by freely combining the generating morphisms using backward composition `>>`

(then).

```
from lambeq.backend.grammar import Box, Id, Ty
A, B, C, D = map(Ty, 'ABCD')
f = Box('f', A, B)
g = Box('g', B, C)
h = Box('h', C, D)
# the codomain of f and domain of g match, so f and g compose
f >> g
assert f.cod == g.dom == B
# associativity
assert f >> (g >> h) == f >> g >> h == (f >> g) >> h
# identity
assert Id(A) >> f == f.to_diagram() == f >> Id(B)
```

As mentioned above, in `lambeq`

the generating morphisms are defined using the `Box`

class. When morphisms are composed, they combine to become an `Diagram`

. This explains the need for the `grammar.Box.to_diagram()`

call above as `f`

was declared as a `grammar.Box`

instance and cannot be directly tested for equality with a `grammar.Diagram`

instance. Compared to traditional category theory notation, `lambeq`

prefers to use backwards composition `>>`

, where `f >> g`

should be read as “`f`

followed by `g`

”.

```
# only arrows that 'type-check' can be composed
diagram = f >> g >> h
```

A `Diagram`

behaves like a `List[Diagram]`

: it can be indexed, sliced, or even reversed. Reversing a morphism actually performs the *dagger* operation, which is the abstract notion of a dagger in quantum mechanics and linear algebra.

```
print(diagram)
print(f'Indexing:', diagram[0])
print(f'Slicing:', diagram[1:])
print(f'Reversing (dagger):', diagram[::-1])
```

```
|Ty() @ [f; Ty(A) -> Ty(B)] @ Ty()| >> |Ty() @ [g; Ty(B) -> Ty(C)] @ Ty()| >> |Ty() @ [h; Ty(C) -> Ty(D)] @ Ty()|
Indexing: |Ty() @ [f; Ty(A) -> Ty(B)] @ Ty()|
Slicing: |Ty() @ [g; Ty(B) -> Ty(C)] @ Ty()| >> |Ty() @ [h; Ty(C) -> Ty(D)] @ Ty()|
Reversing (dagger): |Ty() @ [h†; Ty(D) -> Ty(C)] @ Ty()| >> |Ty() @ [g†; Ty(C) -> Ty(B)] @ Ty()| >> |Ty() @ [f†; Ty(B) -> Ty(A)] @ Ty()|
```

## Monoidal categories¶

A *monoidal category* is a category equipped with the *monoidal product* \(\otimes\) and *monoidal unit* \(I\) and has the following properties:

objects can be combined to return another object (e.g \(A \otimes B\))

morphisms can be combined to return another morphism (\((f: A \to B) \otimes (g: C \to D) = f \otimes g: A \otimes C \to B \otimes D\)).

\(\otimes\) is associative on objects: \((A \otimes B) \otimes C = A \otimes (B \otimes C)\)

\(\otimes\) is associative on morphisms: \((f \otimes g) \otimes h = f \otimes (g \otimes h)\)

\(I\) is the identity on objects for \(\otimes\): \(A \otimes I= A = I \otimes A\)

\(1_I\) is the identity on arrows for \(\otimes\): \(f \otimes 1_I = f = 1_I \otimes f\)

For monoidal categories: again, the generating objects are defined with the `Ty`

class, and the generating morphisms with the `Box`

class; the composite objects are built using `@`

and the composite morphisms using `>>`

.

```
from lambeq.backend.grammar import Box, Id, Ty
A, B, C = Ty('A'), Ty('B'), Ty('C')
f = Box('f', A, B)
g = Box('g', B, C)
h = Box('h', B, A)
# combining types
A @ B
# combining boxes
f @ g
# associativity
assert (A @ B) @ C == A @ B @ C == A @ (B @ C)
assert (f @ g) @ h == f @ g @ h == f @ (g @ h)
# monoidal unit
assert A @ Ty() == A == Ty() @ A
assert f @ Id(Ty()) == f.to_diagram() == Id(Ty()) @ f
```

Monoidal categories have an elegant graphical calculus, which allow them to be drawn and manipulated graphically.

```
x = Box('x', A, A)
y = Box('y', A @ A, B)
diagram = x @ Id(A) >> y
print(repr(diagram))
diagram.draw(figsize=(5, 3))
```

```
|Ty() @ [x; Ty(A) -> Ty(A)] @ Ty(A)| >> |Ty() @ [y; Ty(A) @ Ty(A) -> Ty(B)] @ Ty()|
```

A `Ty`

can be indexed, sliced, or even reversed, just like a `List[Ty]`

.

```
t = A @ B @ C
print(t)
print(repr(t))
print('Indexing:', t[0])
print(f'Slicing:', t[1:])
print(f'Reversing:', t[::-1])
```

```
A @ B @ C
Ty(A) @ Ty(B) @ Ty(C)
Indexing: A
Slicing: B @ C
Reversing: C @ B @ A
```

Again, a `grammar.Diagram`

behaves like a `List[Diagram]`

, so it can be indexed, sliced, and reversed. Reversing a diagram performs the dagger operation.

```
print(diagram)
print(f'Indexing:', diagram[0])
print(f'Slicing:', diagram[1:])
print(f'Reversing (dagger):', diagram[::-1])
from lambeq.backend.drawing import draw_equation
print('\nDagger operation:')
# boxes are drawn as trapeziums to demonstrate the reflection along the horizontal axis
draw_equation(diagram, diagram[::-1], symbol='->', figsize=(8, 3), asymmetry=0.2)
```

```
|Ty() @ [x; Ty(A) -> Ty(A)] @ Ty(A)| >> |Ty() @ [y; Ty(A) @ Ty(A) -> Ty(B)] @ Ty()|
Indexing: |Ty() @ [x; Ty(A) -> Ty(A)] @ Ty(A)|
Slicing: |Ty() @ [y; Ty(A) @ Ty(A) -> Ty(B)] @ Ty()|
Reversing (dagger): |Ty() @ [y†; Ty(B) -> Ty(A) @ Ty(A)] @ Ty()| >> |Ty() @ [x†; Ty(A) -> Ty(A)] @ Ty(A)|
Dagger operation:
```

A monoidal category equipped with a `Swap`

is known as a symmetric monoidal category. Nested swaps can be defined using the `swap()`

method.

```
from lambeq.backend.grammar import Diagram, Swap
Swap(A, B).draw(figsize=(1, 1), draw_as_pregroup=False)
Diagram.swap(A @ B, C).draw(figsize=(2, 2), draw_as_pregroup=False)
```

Note

In a strict mathematical sense, the associativity and unit rules of \(\otimes\) in a monoidal category only hold up to *isomorphism*. As a consequence, this definition requires extra morphisms such as unitors and associators, as well as complicated coherence conditions. Instead, `lambeq`

strictly enforces the rules to hold up to equality, so such coherence conditions are unnecessary. This greatly simplifies its practical use.

## Rigid monoidal categories¶

A *rigid category* is a monoidal category where every object \(A\) has a *left adjoint* \(A^l\) and *right adjoint* \(A^r\). The left adjoint of the right adjoint of a type is equal to the type itself, and vice versa: \((A^r)^l = A = (A^l)^r\)

In `lambeq`

, the adjoint of a type `Ty`

is obtained using the `.l`

and `.r`

properties:

```
from lambeq.backend.grammar import Box, Id, Ty
A = Ty('A')
print(A.l, 'is represented as', repr(A.l))
print(A.r, 'is represented as', repr(A.r))
assert A.r.l == A == A.l.r
```

```
A.l is represented as Ty(A).l
A.r is represented as Ty(A).r
```

The key property of a rigid category is the existence of cups and caps between an object and its adjoint: these are special morphisms that are drawn as bent wires in diagrammatic notation.

```
from lambeq.backend.grammar import Cap, Cup
draw_equation(Cup(A.r, A.r.r), Cup(A, A.r), Cup(A.l, A), symbol='...', figsize=(8, 1))
draw_equation(Cap(A.l, A.l.l), Cap(A, A.l), Cap(A.r, A), symbol='...', figsize=(8, 1))
```

Cups and caps satisfy the so-called snake equations:

```
snake1 = Id(A) @ Cap(A.r, A) >> Cup(A, A.r) @ Id(A)
snake2 = Cap(A, A.l) @ Id(A) >> Id(A) @ Cup(A.l, A)
assert snake1.normal_form() == Id(A) == snake2.normal_form()
print('Snake Equations - For any object', A, ':')
draw_equation(snake1, Id(A), snake2, figsize=(8, 2))
```

```
Snake Equations - For any object A :
```

Note

The `grammar.Diagram.normal_form()`

method used above also applies on standard monoidal diagrams.

Nested cups and caps can be created using the `grammar.Diagram.cups()`

and `grammar.Diagram.caps()`

methods.

```
from lambeq.backend.grammar import Diagram
A, B = Ty('A'), Ty('B')
nested_cup = Diagram.cups(A @ B, (A @ B).r)
nested_cap = Diagram.caps((A @ B).r, A @ B)
nested_snake = Id(A @ B) @ nested_cap >> nested_cup @ Id(A @ B)
assert nested_snake.normal_form() == Id(A @ B)
draw_equation(nested_snake, nested_snake.normal_form())
```