Source code for discopy.grammar.abstract

# -*- coding: utf-8 -*-

"""
Abstract categorial grammars after de Groote's `Towards abstract categorial
grammars (2001) <https://aclanthology.org/P01-1033/>`_: a vocabulary
generates a free closed category and a :class:`Lexicon` is a functor between
two of them, which is all there is to a grammar.

Summary
-------

.. autosummary::
    :template: class.rst
    :nosignatures:
    :toctree:

    Ty
    Exp
    Diagram
    Box
    Eval
    Coeval
    Curry
    Copy
    Discard
    Permutation
    Swap
    Trace
    Sum
    Functor
    Lexicon
    TermBase
    Constant
    Variable
    Application
    Abstraction

Vocabularies and lexicons
-------------------------

A *vocabulary* is a higher-order signature: atomic types and constants, each
with an implicative type ``x >> y`` built from the atoms, e.g. the words of a
language with their grammatical types. It generates a free closed category,
whose morphisms are the lambda terms of its internal language, here
:class:`Term`. Nothing forces a term to be linear: the closed categories of
DisCoPy are markov by design, so a variable may be copied and discarded, as
the variables of ground type are in
the semantics of the example below; the paper's linear terms are the special
case where every variable occurs once, see ``is_linear``. A :class:`Lexicon`
from one vocabulary to another sends atomic types to types and constants to
terms of the image of their types: it is a :class:`Functor` between two free
closed categories, and lexicons compose. A lexicon is all there is to a
grammar: its abstract vocabulary is the atoms and constants it is defined on,
its abstract language the closed terms over them and its object language their
image. The paper's distinguished type ``s`` picks the sentences out of the
abstract language, the input of a parser to come.

Strings
-------

Strings are the paper's, section 4: one atomic type :data:`Position` and a
string is a map from positions to positions, a term of type
``String = Position >> Position``. A word is a constant of that type, the
empty string is the identity ``Position(lambda x: x)`` and concatenation is
composition, ``John.compose(seeks)`` for *John seeks*, see
:meth:`discopy.closed.TermBase.compose`: the paper writes it
``lambda z. x (y z)`` with the first word applied last, here the first word
is applied first, so a string reads its positions in order.

Example
-------

The two readings of Montague's *John seeks a unicorn* share one syntax, with
a constant for each reading of *seeks* in the abstract vocabulary:

>>> n, np, s = map(Ty, ("n", "np", "s"))
>>> J, U, A = np("J"), n("U"), (n >> np)("A")
>>> S_re, S_dicto = ((np >> (np >> s))(S) for S in ("S_re", "S_dicto"))
>>> John, seeks, a, unicorn = map(
...     String, ("John", "seeks", "a", "unicorn"))
>>> seek = String(lambda x: String(lambda y: x.compose(seeks, y)))
>>> syntax = Lexicon(
...     ob_map={n: String, np: String, s: String},
...     ar_map={J: John, U: unicorn, A: String(lambda x: a.compose(x)),
...             S_re: seek, S_dicto: seek})
>>> from discopy.python import Function
>>> words = Functor(
...     {Position: list}, lambda w: lambda: lambda xs: xs + [w.name],
...     cod=Function)
>>> for reading in (S_re(J)(A(U)), S_dicto(J)(A(U))):
...     print(*words(syntax(reading))()([]))
John seeks a unicorn
John seeks a unicorn

The paper's semantic lexicon reads noun phrases as quantifiers. The image of
*a* copies its variable of ground type, so the semantic terms are not linear,
and the two readings are two closed terms of type ``t``:

>>> e, t = Ty("e"), Ty("t")
>>> Predicate, Quantifier = e >> t, (e >> t) >> t
>>> JOHN, UNICORN = e("JOHN"), Predicate("UNICORN")
>>> TRY_TO, FIND = (e >> Quantifier)("TRY_TO"), (e >> Predicate)("FIND")
>>> exists, and_ = Quantifier("exists"), (t >> (t >> t))("and")
>>> some = Predicate(lambda P: Predicate(lambda Q: exists(
...     e(lambda x: and_(P(x))(Q(x))))))
>>> seek_re = Quantifier(lambda P: Quantifier(lambda Q: Q(e(lambda x: P(
...     e(lambda y: TRY_TO(y)(e(lambda z: FIND(z)(x)))))))))
>>> seek_dicto = Quantifier(lambda P: Quantifier(lambda Q: P(e(lambda x:
...     TRY_TO(x)(e(lambda y: Q(e(lambda z: FIND(y)(z)))))))))
>>> semantics = Lexicon(
...     ob_map={n: Predicate, np: Quantifier, s: t},
...     ar_map={J: Predicate(lambda P: P(JOHN)), U: UNICORN,
...             A: some, S_re: seek_re, S_dicto: seek_dicto})
>>> assert not some.is_linear
>>> de_re, de_dicto = semantics(S_re(J)(A(U))), semantics(S_dicto(J)(A(U)))
>>> assert de_re != de_dicto and de_re.cod == de_dicto.cod == t
>>> assert not de_re.freevars and not de_dicto.freevars

Categorial grammars
-------------------

A categorial grammar is a vocabulary of its own, a planar one: a lexicon out
of it is a :class:`discopy.grammar.categorial.Functor` into abstract terms,
sending each atom to a type and each word to a term of the image of its type,
its strings written per word like every lexicon of the paper.
:meth:`Diagram.from_categorial` forgets planarity instead, collapsing left and
right exponentials into the one exponential of a closed category, so that a
derivation becomes a closed term: application evaluates, composition and type
raising curry, and crossed composition composes.

>>> from discopy.grammar import categorial
>>> N, S = categorial.Ty("n"), categorial.Ty("s")
>>> Alice, loves, Bob = N("Alice"), ((N >> S) << N)("loves"), N("Bob")
>>> LOVES = String("loves")
>>> strings = categorial.Functor(
...     ob_map={N: String, S: String},
...     ar_map={Alice: String("Alice"), Bob: String("Bob"),
...             loves: String(lambda o: String(
...                 lambda x: x.compose(LOVES, o)))},
...     cod=Diagram)
>>> print(*words(strings(Alice(loves(Bob), left=True)))()([]))
Alice loves Bob
>>> print(Alice(loves(Bob), left=True).to_abstract())
loves(Bob)(Alice)
"""

from __future__ import annotations

from discopy import closed, cmap, hypergraph
from discopy.cat import factory
from discopy.grammar import categorial


[docs] @factory class Ty(closed.Ty): "An implicative type, the type of an abstract categorial grammar."
[docs] @classmethod def from_categorial(cls, old: categorial.Ty) -> Ty: """ Translate a categorial type into an abstract type, collapsing left and right exponentials into a single exponential. Parameters: old : The categorial type to translate. """ return cls.from_biclosed(old)
[docs] class Exp(closed.Exp): "An exponential object in an abstract categorial grammar." ob = Ty
[docs] @factory class Diagram(closed.Diagram): """ An abstract diagram is a closed diagram with words as boxes, i.e. a derivation in an abstract categorial grammar. The rules of a categorial grammar are its closed structure: application is evaluation, composition is the currying of two evaluations and crossed composition coincides with composition, since a closed category has one exponential. Parameters: inside(Layer) : The layers inside the diagram. dom (Ty) : The domain of the diagram, i.e. its input. cod (Ty) : The codomain of the diagram, i.e. its output. """ ob = Ty
[docs] @classmethod def fa(cls, left: Ty, right: Ty) -> Diagram: "Forward application ``(left << right) @ right -> left``." return cls.ev(left, right, left=True)
[docs] @classmethod def ba(cls, left: Ty, right: Ty) -> Diagram: "Backward application ``left @ (left >> right) -> right``." return cls.ev(right, left, left=False)
[docs] @classmethod def fc(cls, left: Ty, middle: Ty, right: Ty) -> Diagram: """ Forward composition ``(left << middle) @ (middle << right) -> left << right``. """ return (cls.id(left << middle) @ cls.fa(middle, right) >> cls.fa(left, middle)).curry(len(right), left=True)
[docs] @classmethod def bc(cls, left: Ty, middle: Ty, right: Ty) -> Diagram: """ Backward composition ``(left >> middle) @ (middle >> right) -> left >> right``. """ return (cls.ba(left, middle) @ cls.id(middle >> right) >> cls.ba(middle, right)).curry(len(left), left=False)
[docs] @classmethod def fx(cls, left: Ty, middle: Ty, right: Ty) -> Diagram: "Forward crossed composition, which coincides with :meth:`fc`." return cls.fc(left, middle, right)
[docs] @classmethod def bx(cls, left: Ty, middle: Ty, right: Ty) -> Diagram: "Backward crossed composition, which coincides with :meth:`bc`." return cls.bc(left, middle, right)
[docs] @classmethod def from_categorial(cls, diagram: categorial.Diagram) -> Diagram: """ The abstract diagram of a categorial diagram, or the abstract term of a categorial term, dropping planarity: left and right exponentials collapse, constants stay constants and words become boxes, crossed compositions become compositions and the composition and type-raising terms become lambda terms. Parameters: diagram : The categorial diagram or term to translate. Example ------- >>> from discopy.grammar import categorial >>> n, s = categorial.Ty("n"), categorial.Ty("s") >>> Alice, loves, Bob = n("Alice"), ((n >> s) << n)("loves"), n("Bob") >>> print(Diagram.from_categorial(Alice(loves(Bob), left=True))) loves(Bob)(Alice) """ functor = categorial.Functor( ob_map=lambda x: cls.ob(x.inside[0].name), ar_map=lambda box: cls.ob.constant_factory( box.name, functor(box.cod)) if isinstance(box, categorial.Constant) else Box(box.name, functor(box.dom), functor(box.cod)), cod=cls) return functor(diagram)
[docs] class Box(closed.Box, Diagram): "An abstract box is a closed box in an abstract diagram."
[docs] class Eval(closed.Eval, Box): "The evaluation of an exponential type."
[docs] class Coeval(closed.Coeval, Box): "The coevaluation of an exponential type, i.e. the dagger of an Eval."
[docs] class Curry(closed.Curry, Box): "The currying of an abstract diagram."
[docs] class Copy(closed.Copy, Box): "The copy of an abstract type, or its discard when ``n=0``."
[docs] class Discard(closed.Discard, Copy): "The discard of an abstract type, i.e. a copy with zero legs."
[docs] class Permutation(closed.Permutation, Box): "A permutation of abstract types."
[docs] class Swap(Permutation, closed.Swap, Box): "The symmetric swap of two abstract types."
[docs] class Trace(closed.Trace, Box): "A trace in an abstract categorial grammar."
[docs] class Sum(closed.Sum, Box): "A formal sum of abstract diagrams."
[docs] class Functor(closed.Functor): """ An abstract functor is a closed functor with abstract diagrams as domain. 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
[docs] class Lexicon(Functor): """ A lexicon is a functor from one vocabulary to another, i.e. between two free closed categories: it sends atomic types to types and constants to terms of the image of their types, section 2.2 of de Groote's `Towards abstract categorial grammars (2001) <https://aclanthology.org/P01-1033/>`_. Parameters: ob_map (Mapping[Ty, Ty]) : Map from atomic types to types. ar_map (Mapping[Constant, Term]) : Map from constants to terms. Lexicons compose: ``first >> second`` sends a constant ``c`` to ``second(first(c))``, with the target vocabulary of the first lexicon serving as the source vocabulary of the second. Example ------- >>> x, y, z = map(Ty, "xyz") >>> first = Lexicon(ob_map={x: y}, ar_map={x("a"): y("b")}) >>> second = Lexicon(ob_map={y: z}, ar_map={y("b"): z("c")}) >>> assert first(x("a")) == y("b") and (first >> second)(x("a")) == z("c") >>> Lexicon(ob_map={x: y}, ar_map={x("a"): (y >> y)("b")})(x("a")) Traceback (most recent call last): ... discopy.utils.AxiomError: Expected a term of type y for a, got ... """ dom = cod = Diagram
CMap = cmap.CMap[Diagram]
[docs] class TermBase(Box, closed.TermBase): "A term in the internal language of an abstract categorial grammar." functor = Functor.id(Diagram)
[docs] class Constant(TermBase, closed.Constant): "A constant of the vocabulary of an abstract categorial grammar."
Word = Constant
[docs] class Variable(TermBase, closed.Variable): "A variable term in an abstract categorial grammar."
[docs] class Application(TermBase, closed.Application): "The application of an abstract term to another."
[docs] class Abstraction(TermBase, closed.Abstraction): "The abstraction of a variable in an abstract term."
type Term = Constant | Variable | Application | Abstraction Id = Diagram.id Diagram.functor_factory = Functor Diagram.copy_factory = Copy Diagram.permutation_factory = Permutation Diagram.swap_factory = Swap Diagram.curry_factory = Curry Diagram.eval_factory = Eval Diagram.coeval_factory = Coeval Diagram.trace_factory = Trace Diagram.discard_factory = Discard Diagram.sum_factory = Sum Ty.exp_factory = Ty.under_factory = Ty.over_factory = staticmethod(Exp) Ty.variable_factory = Variable Ty.constant_factory = Constant Ty.application_factory = Application Ty.abstraction_factory = Abstraction Hypergraph = hypergraph.Hypergraph[Diagram] Position = Ty("o") """ The atomic type of positions in a string, an arbitrary atom. """ String = Position >> Position """ The type of strings, maps from positions to positions: a word is a constant of type ``String``, ``Position(lambda x: x)`` is the empty string and ``John.compose(seeks)`` their concatenation, see :meth:`discopy.closed.TermBase.compose`. """