343 lines
11 KiB
Python
343 lines
11 KiB
Python
# Natural Language Toolkit: Combinatory Categorial Grammar
|
|
#
|
|
# Copyright (C) 2001-2018 NLTK Project
|
|
# Author: Graeme Gange <ggange@csse.unimelb.edu.au>
|
|
# URL: <http://nltk.org/>
|
|
# For license information, see LICENSE.TXT
|
|
"""
|
|
CCG Combinators
|
|
"""
|
|
|
|
from __future__ import unicode_literals
|
|
from abc import ABCMeta, abstractmethod
|
|
from six import add_metaclass
|
|
|
|
from nltk.compat import python_2_unicode_compatible
|
|
from nltk.ccg.api import FunctionalCategory
|
|
|
|
|
|
@add_metaclass(ABCMeta)
|
|
class UndirectedBinaryCombinator(object):
|
|
"""
|
|
Abstract class for representing a binary combinator.
|
|
Merely defines functions for checking if the function and argument
|
|
are able to be combined, and what the resulting category is.
|
|
|
|
Note that as no assumptions are made as to direction, the unrestricted
|
|
combinators can perform all backward, forward and crossed variations
|
|
of the combinators; these restrictions must be added in the rule
|
|
class.
|
|
"""
|
|
@abstractmethod
|
|
def can_combine(self, function, argument):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def combine(self, function, argument):
|
|
pass
|
|
|
|
|
|
@add_metaclass(ABCMeta)
|
|
class DirectedBinaryCombinator(object):
|
|
"""
|
|
Wrapper for the undirected binary combinator.
|
|
It takes left and right categories, and decides which is to be
|
|
the function, and which the argument.
|
|
It then decides whether or not they can be combined.
|
|
"""
|
|
@abstractmethod
|
|
def can_combine(self, left, right):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def combine(self, left, right):
|
|
pass
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class ForwardCombinator(DirectedBinaryCombinator):
|
|
"""
|
|
Class representing combinators where the primary functor is on the left.
|
|
|
|
Takes an undirected combinator, and a predicate which adds constraints
|
|
restricting the cases in which it may apply.
|
|
"""
|
|
def __init__(self, combinator, predicate, suffix=''):
|
|
self._combinator = combinator
|
|
self._predicate = predicate
|
|
self._suffix = suffix
|
|
|
|
def can_combine(self, left, right):
|
|
return (self._combinator.can_combine(left, right) and
|
|
self._predicate(left, right))
|
|
|
|
def combine(self, left, right):
|
|
for cat in self._combinator.combine(left, right):
|
|
yield cat
|
|
|
|
def __str__(self):
|
|
return ">%s%s" % (self._combinator, self._suffix)
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class BackwardCombinator(DirectedBinaryCombinator):
|
|
"""
|
|
The backward equivalent of the ForwardCombinator class.
|
|
"""
|
|
def __init__(self, combinator, predicate, suffix=''):
|
|
self._combinator = combinator
|
|
self._predicate = predicate
|
|
self._suffix = suffix
|
|
|
|
def can_combine(self, left, right):
|
|
return (self._combinator.can_combine(right, left) and
|
|
self._predicate(left, right))
|
|
|
|
def combine(self, left, right):
|
|
for cat in self._combinator.combine(right, left):
|
|
yield cat
|
|
|
|
def __str__(self):
|
|
return "<%s%s" % (self._combinator, self._suffix)
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class UndirectedFunctionApplication(UndirectedBinaryCombinator):
|
|
"""
|
|
Class representing function application.
|
|
Implements rules of the form:
|
|
X/Y Y -> X (>)
|
|
And the corresponding backwards application rule
|
|
"""
|
|
|
|
def can_combine(self, function, argument):
|
|
if not function.is_function():
|
|
return False
|
|
|
|
return not function.arg().can_unify(argument) is None
|
|
|
|
def combine(self, function, argument):
|
|
if not function.is_function():
|
|
return
|
|
|
|
subs = function.arg().can_unify(argument)
|
|
if subs is None:
|
|
return
|
|
|
|
yield function.res().substitute(subs)
|
|
|
|
def __str__(self):
|
|
return ''
|
|
|
|
|
|
# Predicates for function application.
|
|
|
|
# Ensures the left functor takes an argument on the right
|
|
def forwardOnly(left, right):
|
|
return left.dir().is_forward()
|
|
|
|
|
|
# Ensures the right functor takes an argument on the left
|
|
def backwardOnly(left, right):
|
|
return right.dir().is_backward()
|
|
|
|
|
|
# Application combinator instances
|
|
ForwardApplication = ForwardCombinator(UndirectedFunctionApplication(),
|
|
forwardOnly)
|
|
BackwardApplication = BackwardCombinator(UndirectedFunctionApplication(),
|
|
backwardOnly)
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class UndirectedComposition(UndirectedBinaryCombinator):
|
|
"""
|
|
Functional composition (harmonic) combinator.
|
|
Implements rules of the form
|
|
X/Y Y/Z -> X/Z (B>)
|
|
And the corresponding backwards and crossed variations.
|
|
"""
|
|
def can_combine(self, function, argument):
|
|
# Can only combine two functions, and both functions must
|
|
# allow composition.
|
|
if not (function.is_function() and argument.is_function()):
|
|
return False
|
|
if function.dir().can_compose() and argument.dir().can_compose():
|
|
return not function.arg().can_unify(argument.res()) is None
|
|
return False
|
|
|
|
def combine(self, function, argument):
|
|
if not (function.is_function() and argument.is_function()):
|
|
return
|
|
if function.dir().can_compose() and argument.dir().can_compose():
|
|
subs = function.arg().can_unify(argument.res())
|
|
if subs is not None:
|
|
yield FunctionalCategory(function.res().substitute(subs),
|
|
argument.arg().substitute(subs),
|
|
argument.dir())
|
|
|
|
def __str__(self):
|
|
return 'B'
|
|
|
|
|
|
# Predicates for restricting application of straight composition.
|
|
def bothForward(left, right):
|
|
return left.dir().is_forward() and right.dir().is_forward()
|
|
|
|
|
|
def bothBackward(left, right):
|
|
return left.dir().is_backward() and right.dir().is_backward()
|
|
|
|
|
|
# Predicates for crossed composition
|
|
def crossedDirs(left, right):
|
|
return left.dir().is_forward() and right.dir().is_backward()
|
|
|
|
|
|
def backwardBxConstraint(left, right):
|
|
# The functors must be crossed inwards
|
|
if not crossedDirs(left, right):
|
|
return False
|
|
# Permuting combinators must be allowed
|
|
if not left.dir().can_cross() and right.dir().can_cross():
|
|
return False
|
|
# The resulting argument category is restricted to be primitive
|
|
return left.arg().is_primitive()
|
|
|
|
|
|
# Straight composition combinators
|
|
ForwardComposition = ForwardCombinator(UndirectedComposition(),
|
|
forwardOnly)
|
|
BackwardComposition = BackwardCombinator(UndirectedComposition(),
|
|
backwardOnly)
|
|
|
|
# Backward crossed composition
|
|
BackwardBx = BackwardCombinator(UndirectedComposition(), backwardBxConstraint,
|
|
suffix='x')
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class UndirectedSubstitution(UndirectedBinaryCombinator):
|
|
"""
|
|
Substitution (permutation) combinator.
|
|
Implements rules of the form
|
|
Y/Z (X\Y)/Z -> X/Z (<Sx)
|
|
And other variations.
|
|
"""
|
|
def can_combine(self, function, argument):
|
|
if function.is_primitive() or argument.is_primitive():
|
|
return False
|
|
|
|
# These could potentially be moved to the predicates, as the
|
|
# constraints may not be general to all languages.
|
|
if function.res().is_primitive():
|
|
return False
|
|
if not function.arg().is_primitive():
|
|
return False
|
|
|
|
if not (function.dir().can_compose() and argument.dir().can_compose()):
|
|
return False
|
|
return (function.res().arg() == argument.res()) and (function.arg() == argument.arg())
|
|
|
|
def combine(self, function, argument):
|
|
if self.can_combine(function, argument):
|
|
yield FunctionalCategory(function.res().res(), argument.arg(),
|
|
argument.dir())
|
|
|
|
def __str__(self):
|
|
return 'S'
|
|
|
|
|
|
# Predicate for forward substitution
|
|
def forwardSConstraint(left, right):
|
|
if not bothForward(left, right):
|
|
return False
|
|
return left.res().dir().is_forward() and left.arg().is_primitive()
|
|
|
|
|
|
# Predicate for backward crossed substitution
|
|
def backwardSxConstraint(left, right):
|
|
if not left.dir().can_cross() and right.dir().can_cross():
|
|
return False
|
|
if not bothForward(left, right):
|
|
return False
|
|
return right.res().dir().is_backward() and right.arg().is_primitive()
|
|
|
|
|
|
# Instances of substitution combinators
|
|
ForwardSubstitution = ForwardCombinator(UndirectedSubstitution(),
|
|
forwardSConstraint)
|
|
BackwardSx = BackwardCombinator(UndirectedSubstitution(),
|
|
backwardSxConstraint, 'x')
|
|
|
|
|
|
# Retrieves the left-most functional category.
|
|
# ie, (N\N)/(S/NP) => N\N
|
|
def innermostFunction(categ):
|
|
while categ.res().is_function():
|
|
categ = categ.res()
|
|
return categ
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class UndirectedTypeRaise(UndirectedBinaryCombinator):
|
|
"""
|
|
Undirected combinator for type raising.
|
|
"""
|
|
def can_combine(self, function, arg):
|
|
# The argument must be a function.
|
|
# The restriction that arg.res() must be a function
|
|
# merely reduces redundant type-raising; if arg.res() is
|
|
# primitive, we have:
|
|
# X Y\X =>(<T) Y/(Y\X) Y\X =>(>) Y
|
|
# which is equivalent to
|
|
# X Y\X =>(<) Y
|
|
if not (arg.is_function() and arg.res().is_function()):
|
|
return False
|
|
|
|
arg = innermostFunction(arg)
|
|
|
|
# left, arg_categ are undefined!
|
|
subs = left.can_unify(arg_categ.arg())
|
|
if subs is not None:
|
|
return True
|
|
return False
|
|
|
|
def combine(self, function, arg):
|
|
if not (function.is_primitive() and
|
|
arg.is_function() and arg.res().is_function()):
|
|
return
|
|
|
|
# Type-raising matches only the innermost application.
|
|
arg = innermostFunction(arg)
|
|
|
|
subs = function.can_unify(arg.arg())
|
|
if subs is not None:
|
|
xcat = arg.res().substitute(subs)
|
|
yield FunctionalCategory(xcat,
|
|
FunctionalCategory(xcat, function,
|
|
arg.dir()),
|
|
-(arg.dir()))
|
|
|
|
def __str__(self):
|
|
return 'T'
|
|
|
|
|
|
# Predicates for type-raising
|
|
# The direction of the innermost category must be towards
|
|
# the primary functor.
|
|
# The restriction that the variable must be primitive is not
|
|
# common to all versions of CCGs; some authors have other restrictions.
|
|
def forwardTConstraint(left, right):
|
|
arg = innermostFunction(right)
|
|
return arg.dir().is_backward() and arg.res().is_primitive()
|
|
|
|
|
|
def backwardTConstraint(left, right):
|
|
arg = innermostFunction(left)
|
|
return arg.dir().is_forward() and arg.res().is_primitive()
|
|
|
|
|
|
# Instances of type-raising combinators
|
|
ForwardT = ForwardCombinator(UndirectedTypeRaise(), forwardTConstraint)
|
|
BackwardT = BackwardCombinator(UndirectedTypeRaise(), backwardTConstraint)
|