# -*- coding: utf-8 -*-
"""
A categorial grammar is a free biclosed category with words as boxes.
Summary
-------
.. autosummary::
:template: class.rst
:nosignatures:
:toctree:
TermBase
Constant
Variable
Abstraction
FA
BA
FC
BC
FX
BX
Diagram
Box
Word
ForwardCrossedComposition
BackwardCrossedComposition
Functor
CMap
.. admonition:: Functions
.. autosummary::
:template: function.rst
:nosignatures:
:toctree:
cat2ty
tree2diagram
"""
from __future__ import annotations
from dataclasses import dataclass
import re
from discopy import biclosed, cmap, messages
from discopy.abc import ClosedCategory
from discopy.cat import factory
from discopy.grammar import thue
from discopy.utils import (
BinaryBoxConstructor,
AxiomError,
factory_name,
get_origin,
)
@factory
class Ty(biclosed.Ty):
"Base class for categorial grammar types."
class Over(biclosed.Over):
"Categorial grammar type ``base << exponent``."
ob = Ty
class Under(biclosed.Under):
"Categorial grammar type ``exponent >> base``."
ob = Ty
[docs]
@factory
class Diagram(biclosed.Diagram):
"""
A categorial diagram is a biclosed diagram with rules and words as boxes.
"""
ob = Ty
def to_pregroup(self):
from discopy.grammar import pregroup
return Functor(
ob_map=lambda x: pregroup.Ty(x.inside[0].name),
ar_map=lambda f: pregroup.Box(f.name,
Diagram.to_pregroup(f.dom),
Diagram.to_pregroup(f.cod)),
cod=pregroup.Diagram)(self)
[docs]
@staticmethod
def fa(left, right):
""" Forward application. """
return Eval(left << right)
[docs]
@staticmethod
def ba(left, right):
""" Backward application. """
return Eval(left >> right)
[docs]
@staticmethod
def fc(left, middle, right):
""" Forward composition. """
return (
(left << middle) @ Eval(middle << right) >> Eval(left << middle)
).curry(n=len(right), left=True)
[docs]
@staticmethod
def bc(left, middle, right):
""" Backward composition. """
return (
Eval(left >> middle) @ (middle >> right) >> Eval(middle >> right)
).curry(n=len(left), left=False)
[docs]
@staticmethod
def fx(left, middle, right):
""" Forward crossed composition. """
return ForwardCrossedComposition(left << middle, right >> middle)
[docs]
@staticmethod
def bx(left, middle, right):
""" Backward crossed composition. """
return BackwardCrossedComposition(middle << left, middle >> right)
[docs]
class Box(biclosed.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 (biclosed.Ty) : The grammatical type of the word.
dom (biclosed.Ty) : An optional domain for the word, empty by default.
"""
class Eval(biclosed.Eval, Box):
"""
Evaluation box in a categorial grammar.
"""
class Curry(biclosed.Curry, Box):
"""
The currying of a categorial diagram.
"""
[docs]
class ForwardCrossedComposition(BinaryBoxConstructor, Box):
""" Forward crossed composition rule. """
def __init__(self, left, right):
assert left.is_over
assert right.is_under
if left.exponent != right.base:
raise AxiomError(messages.NOT_COMPOSABLE.format(
left, right, left.exponent, right.base))
name = f"ForwardCrossedComposition({left}, {right})"
dom, cod = left @ right, right.exponent >> left.base
Box.__init__(self, name, dom, cod)
BinaryBoxConstructor.__init__(self, left, right)
[docs]
class BackwardCrossedComposition(BinaryBoxConstructor, Box):
""" Backward crossed composition rule. """
def __init__(self, left, right):
assert left.is_over
assert right.is_under
if left.base != right.exponent:
raise AxiomError(messages.NOT_COMPOSABLE.format(
left, right, left.base, right.exponent))
name = f"BackwardCrossedComposition({left}, {right})"
dom, cod = left @ right, right.base << left.exponent
Box.__init__(self, name, dom, cod)
BinaryBoxConstructor.__init__(self, left, right)
[docs]
class Functor(biclosed.Functor):
"""
A categorial functor is a biclosed functor with a predefined mapping
for categorial rules.
Parameters:
ob_map (Mapping[Ty, Ty]) :
Map from atomic :class:`Ty` to :code:`cod.ob`.
ar_map (Mapping[Box, Diagram]) : Map from :class:`Box` to :code:`cod`.
cod (Category) : The codomain of the functor.
"""
dom = cod = Diagram
def __call__(self, other):
if isinstance(other, ForwardCrossedComposition):
left = other.dom.inside[0].left
middle = other.dom.inside[0].right
right = other.dom.inside[1].left
return self.cod.fx(self(left), self(middle), self(right))
if isinstance(other, BackwardCrossedComposition):
left = other.dom.inside[0].right
middle = other.dom.inside[0].left
right = other.dom.inside[1].right
return self.cod.bx(self(left), self(middle), self(right))
return super().__call__(other)
CMap = cmap.CMap[Diagram]
[docs]
class TermBase(Box, biclosed.TermBase):
"""
A term in the internal language of a categorial grammar.
"""
functor = Functor.id(Diagram)
freevars = None
def __call__(self, other, left=False):
return BA(self, other) if left else FA(self, other)
[docs]
def to_abstract(self):
"""
The abstract term of a categorial term, dropping planarity,
see :meth:`discopy.grammar.abstract.Diagram.from_categorial`.
"""
from discopy.grammar.abstract import Diagram
return Diagram.from_categorial(self)
[docs]
class Constant(TermBase, biclosed.Constant):
def __init__(self, name: str, cod: Ty):
biclosed.Constant.__init__(self, name, cod)
TermBase.__init__(self, self.name, self.dom, self.cod)
def simplify(self):
return self
[docs]
class Variable(TermBase, biclosed.Variable):
def simplify(self):
return self
[docs]
class Abstraction(TermBase, biclosed.Abstraction):
var: Variable
body: Term
left: bool = False
def __init__(self, var: Variable, body: Term, left: bool = False):
biclosed.Abstraction.__init__(self, var, body, left)
TermBase.__init__(self, self.name, self.dom, self.cod)
def simplify(self):
return Abstraction(self.var, self.body.simplify(), self.left)
[docs]
class FA(TermBase, biclosed.Application):
"Application of type ``Y`` with subterms of type ``Y << X`` and ``X``."
def __init__(self, func, args):
biclosed.Application.__init__(self, func, args, left=False)
TermBase.__init__(self, self.name, self.dom, self.cod)
def simplify(self):
return self.func.simplify()(self.args.simplify())
[docs]
class BA(TermBase, biclosed.Application):
"Application of type ``Y`` with subterms of type ``X`` and ``X >> Y``."
def __init__(self, args, func):
biclosed.Application.__init__(self, func, args, left=True)
TermBase.__init__(self, self.name, self.dom, self.cod)
def simplify(self):
return self.args.simplify()(self.func.simplify(), left=True)
def __repr__(self):
return factory_name(type(self)) + f"({self.args!r}, {self.func!r})"
class TypeRaising(TermBase):
"Abstract superclass of :class:`FTR` and :class:`BTR`."
base: Ty
child: Term
def __init__(self, base, child, cod):
name = f"{type(self).__name__}({base}, {child})"
self.base, self.child, self.freevars = base, child, child.freevars
super().__init__(name, child.dom, cod)
def eval(self, **kwargs):
return self.simplify().eval(**kwargs)
def map(self, functor, context):
return functor.map_term(self.simplify(), context)
def __repr__(self):
return factory_name(type(self)) + f"({self.base!r}, {self.child!r})"
@property
def constants(self):
return self.child.constants
class FTR(TypeRaising):
"Forward type raising ``Y << (X >> Y)`` with base ``Y`` and child ``X``."
def __init__(self, base, child):
super().__init__(base, child, base << (child.cod >> base))
def simplify(self):
child = self.child.simplify()
f = Variable.fresh(
"f", self.child.cod >> self.base, child)
return Abstraction(f, child(f, left=True))
class BTR(TypeRaising):
"Backward type raising ``(Y << X) >> Y`` with base ``Y`` and child ``X``."
def __init__(self, base, child):
super().__init__(base, child, (base << child.cod) >> base)
def simplify(self):
child = self.child.simplify()
f = Variable.fresh(
"f", self.base << self.child.cod, child)
return Abstraction(f, f(child), left=True)
@dataclass(frozen=True, repr=False)
class BinaryTerm(TermBase):
"Abstract superclass of :class:`FC`, :class:`BC`, :class:`FX`, :class:`BX`"
left: Term
right: Term
def __post_init__(self):
if set(self.left.freevars).intersection(self.right.freevars):
raise ValueError("Expected disjoint free variables.")
object.__setattr__(
self, "freevars", self.left.freevars + self.right.freevars)
object.__setattr__(self, "dom", self.left.dom @ self.right.dom)
def __str__(self):
return f"{type(self).__name__}({self.left}, {self.right})"
def simplify(self):
return type(self)(self.left.simplify(), self.right.simplify())
def eval(self, **kwargs):
return self.simplify().eval(**kwargs)
def map(self, functor, context):
return functor.map_term(self.simplify(), context)
def __repr__(self):
return factory_name(type(self)) + f"({self.left!r}, {self.right!r})"
@property
def constants(self):
return self.left.constants + self.right.constants
[docs]
@dataclass(frozen=True, repr=False)
class FC(BinaryTerm):
"Forward composition ``A << C`` with subterms ``A << B`` and ``B << C``. "
def __post_init__(self):
super().__post_init__()
assert self.left.cod.is_over
assert self.right.cod.is_over
if self.right.cod.base != self.left.cod.exponent:
raise AxiomError(messages.NOT_COMPOSABLE.format(
self.left.cod, self.right.cod,
self.left.cod.exponent, self.right.cod.base))
@property
def cod(self):
return self.left.cod.base << self.right.cod.exponent
def simplify(self):
f, g = self.left.simplify(), self.right.simplify()
x = Variable.fresh(
"x", self.right.cod.exponent, f, g)
return Abstraction(x, f(g(x)))
[docs]
@dataclass(frozen=True, repr=False)
class BC(BinaryTerm):
"Backward composition ``A >> C`` with subterms ``A >> B`` and ``B >> C``."
def __post_init__(self):
super().__post_init__()
assert self.left.cod.is_under
assert self.right.cod.is_under
if self.left.cod.base != self.right.cod.exponent:
raise AxiomError(messages.NOT_COMPOSABLE.format(
self.left.cod, self.right.cod,
self.left.cod.base, self.right.cod.exponent))
@property
def cod(self):
return self.left.cod.exponent >> self.right.cod.base
def simplify(self):
f, g = self.left.simplify(), self.right.simplify()
x = Variable.fresh(
"x", self.left.cod.exponent, f, g)
return Abstraction(x, x(f, left=True)(g, left=True), left=True)
[docs]
@dataclass(frozen=True, repr=False)
class FX(BinaryTerm):
"Forward crossing ``A >> C`` with subterms ``B << A`` and ``B >> C``."
def __post_init__(self):
super().__post_init__()
ForwardCrossedComposition(self.left.cod, self.right.cod)
@property
def cod(self):
return self.right.cod.exponent >> self.left.cod.base
def eval(self, functor=None):
functor, X = functor or self.functor, self.left.cod.base
Y, Z = self.left.cod.exponent, self.right.cod.exponent
f, g = self.left.eval(functor), self.right.eval(functor)
return f @ g >> functor.cod.fx(*map(functor, [X, Y, Z]))
def map(self, functor, context):
if not issubclass(get_origin(functor.cod), ClosedCategory):
raise AxiomError(
"Crossed composition requires a closed codomain.")
ob = functor.cod.ob
f, g = (functor.map_term(term, context)
for term in (self.left, self.right))
var = ob.variable_factory.fresh(
"x", functor(self.right.cod.exponent), f, g)
return ob.abstraction_factory(var, f(g(var)))
[docs]
@dataclass(frozen=True, repr=False)
class BX(BinaryTerm):
"Backward crossing ``A << C`` with subterms ``A << B`` and ``C >> B``."
def __post_init__(self):
super().__post_init__()
BackwardCrossedComposition(self.left.cod, self.right.cod)
@property
def cod(self):
return self.right.cod.base << self.left.cod.exponent
def eval(self, functor=None):
functor, Z = functor or self.functor, self.right.cod.base
X, Y = self.left.cod.exponent, self.left.cod.base
f, g = self.left.eval(functor), self.right.eval(functor)
return f @ g >> functor.cod.bx(*map(functor, [X, Y, Z]))
def map(self, functor, context):
if not issubclass(get_origin(functor.cod), ClosedCategory):
raise AxiomError(
"Crossed composition requires a closed codomain.")
ob = functor.cod.ob
f, g = (functor.map_term(term, context)
for term in (self.left, self.right))
var = ob.variable_factory.fresh(
"x", functor(self.left.cod.exponent), f, g)
return ob.abstraction_factory(var, g(f(var)))
type Term = (
Constant | Variable | Abstraction | FTR | BTR
| FA | BA | FC | BC | FX | BX)
[docs]
def cat2ty(string: str) -> Ty:
r"""
Translate the string representation of a CCG category into DisCoPy.
A category is either an atom, a bracketed category or two categories
around a slash, which associates to the left as in Steedman's
`The Syntactic Process (2000) <https://mitpress.mit.edu/9780262527446>`_.
The features that CCGbank writes in square brackets, e.g. the ``[dcl]`` of
``S[dcl]``, are ignored: they refine an atom rather than change it.
Parameters:
string : The string with slashes representing a CCG category.
Example
-------
>>> print(cat2ty(r"(S[dcl]\NP)/NP"))
((NP >> S) << NP)
>>> assert cat2ty(r"S\NP/NP") == cat2ty(r"(S\NP)/NP")
>>> assert cat2ty(r"(S\NP)\(S\NP)[conj]") == cat2ty(r"(S\NP)\(S\NP)")
"""
def split(string):
par_count = 0
for i, char in reversed(list(enumerate(string))):
if char == ")":
par_count += 1
elif char == "(":
par_count -= 1
elif char in ["\\", "/"] and par_count == 0:
return string[:i], char, string[i + 1:]
return string, None, None
string = re.sub(r'\[[^]]*\]', '', string)
left, slash, right = split(string)
if slash == '\\':
return cat2ty(right) >> cat2ty(left)
if slash == '/':
return cat2ty(left) << cat2ty(right)
if string.startswith('(') and string.endswith(')'):
return cat2ty(string[1:-1])
return Ty(string)
[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':
rule = Diagram.ba(dom[:1], dom.inside[1].base)
elif tree['type'] == 'fa':
rule = Diagram.fa(dom.inside[0].base, dom[1:])
elif tree['type'] == 'fc':
rule = Diagram.fc(
dom.inside[0].base,
dom.inside[0].exponent,
dom.inside[1].exponent)
else:
rule = Box(tree['type'], dom, cod)
return Id().tensor(*children) >> rule
Id = Diagram.id
Diagram.functor_factory = Functor
Diagram.curry_factory = Curry
Diagram.eval_factory = Eval
Ty.variable_factory = Variable
Ty.constant_factory = Constant
Ty.application_factory =\
lambda func, args, left=False: BA(args, func) if left else FA(func, args)
Ty.abstraction_factory = Abstraction
Ty.over_factory, Ty.under_factory = Over, Under