text: # Natural Language Toolkit: Combinatory Categorial Grammar
  #
  # Copyright (C) 2001-2014 NLTK Project
  # Author: Graeme Gange <ggange@csse.unimelb.edu.au>
  # URL: <http://nltk.org/>
  # For license information, see LICENSE.TXT
  from __future__ import unicode_literals
  
  from nltk.compat import python_2_unicode_compatible
  from nltk.ccg.api import FunctionalCategory
  
  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.
      """
      def can_combine(self, function, argument):
          raise NotImplementedError()
  
      def combine (self,function,argument):
          raise NotImplementedError()
  
  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.
      """
      def can_combine(self, left, right):
          raise NotImplementedError()
  
      def combine(self, left, right):
          raise NotImplementedError()
  
  @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 not subs is 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)
  