# -*- coding: utf-8 -*-
"""
A categorial grammar is a free closed category with words as boxes.
Summary
-------
.. autosummary::
:template: class.rst
:nosignatures:
:toctree:
Diagram
Box
Word
FA
BA
FC
BC
FX
BX
Functor
.. admonition:: Functions
.. autosummary::
:template: function.rst
:nosignatures:
:toctree:
cat2ty
tree2diagram
"""
import re
from discopy import closed, messages
from discopy.cat import factory, AxiomError
from discopy.grammar import thue
from discopy.closed import Ty, Over, Under
from discopy.utils import (
assert_isinstance, factory_name, from_tree, BinaryBoxConstructor)
[docs]@factory
class Diagram(closed.Diagram):
"""
A categorial diagram is a closed diagram with rules and words as boxes.
"""
def to_pregroup(self):
from discopy.grammar import pregroup
return Functor(
ob=lambda x: pregroup.Ty(x.inside[0].name),
ar=lambda f: pregroup.Box(f.name,
Diagram.to_pregroup(f.dom),
Diagram.to_pregroup(f.cod)),
cod=pregroup.Category())(self)
[docs] @staticmethod
def fa(left, right):
""" Forward application. """
return FA(left << right)
[docs] @staticmethod
def ba(left, right):
""" Backward application. """
return BA(left >> right)
[docs] @staticmethod
def fc(left, middle, right):
""" Forward composition. """
return FC(left << middle, middle << right)
[docs] @staticmethod
def bc(left, middle, right):
""" Backward composition. """
return BC(left >> middle, middle >> right)
[docs] @staticmethod
def fx(left, middle, right):
""" Forward crossed composition. """
return FX(left << middle, right >> middle)
[docs] @staticmethod
def bx(left, middle, right):
""" Backward crossed composition. """
return BX(middle << left, middle >> right)
[docs]class Box(closed.Box, Diagram):
"""
A categorial box is a grammar rule in a categorial diagram.
"""
[docs]class Word(thue.Word, Box):
"""
A categorial word is a rule with a ``name``, a grammatical type as ``cod``
and an optional domain ``dom``.
Parameters:
name (str) : The name of the word.
cod (closed.Ty) : The grammatical type of the word.
dom (closed.Ty) : An optional domain for the word, empty by default.
"""
class Eval(closed.Eval, Box):
"""
Evaluation box in a categorial grammar, equivalent to :class:``FA``.
"""
class Curry(closed.Curry, Box):
"""
The currying of a categorial diagram.
"""
def unaryBoxConstructor(attr):
class Constructor:
@classmethod
def from_tree(cls, tree):
return cls(from_tree(tree[attr]))
def to_tree(self):
return {
'factory': factory_name(type(self)),
attr: getattr(self, attr).to_tree()}
return Constructor
[docs]class FA(unaryBoxConstructor("over"), Box):
""" Forward application rule. """
def __init__(self, over):
assert_isinstance(over, Over)
self.over = over
dom, cod = over @ over.exponent, over.base
Box.__init__(self, f"FA{over}", dom, cod)
def __repr__(self):
return f"FA({repr(self.dom[:1])})"
[docs]class BA(unaryBoxConstructor("under"), Box):
""" Backward application rule. """
def __init__(self, under):
assert_isinstance(under, Under)
self.under = under
dom, cod = under.exponent @ under, under.base
Box.__init__(self, f"BA{under}", dom, cod)
def __repr__(self):
return f"BA({repr(self.dom[1:])})"
[docs]class FC(BinaryBoxConstructor, Box):
""" Forward composition rule. """
def __init__(self, left, right):
assert_isinstance(left, Over)
assert_isinstance(right, Over)
if left.exponent != right.base:
raise AxiomError(messages.NOT_COMPOSABLE.format(
left, right, left.exponent, right.base))
name = f"FC({left}, {right})"
dom, cod = left @ right, left.base << right.exponent
Box.__init__(self, name, dom, cod)
BinaryBoxConstructor.__init__(self, left, right)
[docs]class BC(BinaryBoxConstructor, Box):
""" Backward composition rule. """
def __init__(self, left, right):
assert_isinstance(left, Under)
assert_isinstance(right, Under)
if left.base != right.exponent:
raise AxiomError(messages.NOT_COMPOSABLE.format(
left, right, left.base, right.exponent))
name = f"BC({left}, {right})"
dom, cod = left @ right, left.exponent >> right.base
Box.__init__(self, name, dom, cod)
BinaryBoxConstructor.__init__(self, left, right)
[docs]class FX(BinaryBoxConstructor, Box):
""" Forward crossed composition rule. """
def __init__(self, left, right):
assert_isinstance(left, Over)
assert_isinstance(right, Under)
if left.exponent != right.base:
raise AxiomError(messages.NOT_COMPOSABLE.format(
left, right, left.exponent, right.base))
name = f"FX({left}, {right})"
dom, cod = left @ right, right.exponent >> left.base
Box.__init__(self, name, dom, cod)
BinaryBoxConstructor.__init__(self, left, right)
[docs]class BX(BinaryBoxConstructor, Box):
""" Backward crossed composition rule. """
def __init__(self, left, right):
assert_isinstance(left, Over)
assert_isinstance(right, Under)
if left.base != right.exponent:
raise AxiomError(messages.NOT_COMPOSABLE.format(
left, right, left.base, right.exponent))
name = f"BX({left}, {right})"
dom, cod = left @ right, right.base << left.exponent
Box.__init__(self, name, dom, cod)
BinaryBoxConstructor.__init__(self, left, right)
[docs]class Functor(closed.Functor):
"""
A categorial functor is a closed functor with a predefined mapping
for categorial rules.
Parameters:
ob (Mapping[Ty, Ty]) : Map from atomic :class:`Ty` to :code:`cod.ob`.
ar (Mapping[Box, Diagram]) : Map from :class:`Box` to :code:`cod.ar`.
cod (Category) : The codomain of the functor.
"""
dom = cod = closed.Category(Ty, Diagram)
def __call__(self, other):
if isinstance(other, FA):
left, right = other.over.left, other.over.right
return self.cod.ar.fa(self(left), self(right))
if isinstance(other, BA):
left, right = other.under.left, other.under.right
return self.cod.ar.ba(self(left), self(right))
for cls, method in [(FC, 'fc'), (BC, 'bc')]:
if isinstance(other, cls):
left = other.dom.inside[0].left
middle = other.dom.inside[0].right
right = other.dom.inside[1].right
return getattr(self.cod.ar, method)(
self(left), self(middle), self(right))
if isinstance(other, FX):
left = other.dom.inside[0].left
middle = other.dom.inside[0].right
right = other.dom.inside[1].left
return self.cod.ar.fx(self(left), self(middle), self(right))
if isinstance(other, BX):
left = other.dom.inside[0].right
middle = other.dom.inside[0].left
right = other.dom.inside[1].right
return self.cod.ar.bx(self(left), self(middle), self(right))
return super().__call__(other)
[docs]def cat2ty(string: str) -> Ty:
"""
Translate the string representation of a CCG category into DisCoPy.
Parameters:
string : The string with slashes representing a CCG category.
"""
def unbracket(string):
return string[1:-1] if string[0] == '(' else string
def remove_modifier(string):
return re.sub(r'\[[^]]*\]', '', string)
def split(string):
par_count = 0
for i, char in enumerate(string):
if char == "(":
par_count += 1
elif char == ")":
par_count -= 1
elif char in ["\\", "/"] and par_count == 0:
return unbracket(string[:i]), char, unbracket(string[i + 1:])
return remove_modifier(string), None, None
left, slash, right = split(string)
if slash == '\\':
return cat2ty(right) >> cat2ty(left)
if slash == '/':
return cat2ty(left) << cat2ty(right)
return Ty(left)
[docs]def tree2diagram(tree: dict, dom=Ty()) -> Diagram:
"""
Translate a depccg.Tree in JSON format into DisCoPy.
Parameters:
tree : The tree to translate.
dom : The domain for the word boxes, empty by default.
"""
if 'word' in tree:
return Word(tree['word'], cat2ty(tree['cat']), dom=dom)
children = list(map(tree2diagram, tree['children']))
dom = Ty().tensor(*[child.cod for child in children])
cod = cat2ty(tree['cat'])
if tree['type'] == 'ba':
box = BA(dom.inside[1])
elif tree['type'] == 'fa':
box = FA(dom.inside[0])
elif tree['type'] == 'fc':
box = FC(dom.inside[0], dom.inside[1])
else:
box = Box(tree['type'], dom, cod)
return Id().tensor(*children) >> box
Id = Diagram.id
Diagram.curry_factory = Curry
Diagram.eval_factory = Eval