# Natural Language Toolkit: Combinatory Categorial Grammar # # Copyright (C) 2001-2017 NLTK Project # Author: Graeme Gange # URL: # 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 ( 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 =>((>) 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)