linearlogic.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. # Natural Language Toolkit: Linear Logic
  2. #
  3. # Author: Dan Garrette <dhgarrette@gmail.com>
  4. #
  5. # Copyright (C) 2001-2019 NLTK Project
  6. # URL: <http://nltk.org/>
  7. # For license information, see LICENSE.TXT
  8. from __future__ import print_function, unicode_literals
  9. from six import string_types
  10. from nltk.internals import Counter
  11. from nltk.compat import python_2_unicode_compatible
  12. from nltk.sem.logic import LogicParser, APP
  13. _counter = Counter()
  14. class Tokens(object):
  15. # Punctuation
  16. OPEN = '('
  17. CLOSE = ')'
  18. # Operations
  19. IMP = '-o'
  20. PUNCT = [OPEN, CLOSE]
  21. TOKENS = PUNCT + [IMP]
  22. class LinearLogicParser(LogicParser):
  23. """A linear logic expression parser."""
  24. def __init__(self):
  25. LogicParser.__init__(self)
  26. self.operator_precedence = {APP: 1, Tokens.IMP: 2, None: 3}
  27. self.right_associated_operations += [Tokens.IMP]
  28. def get_all_symbols(self):
  29. return Tokens.TOKENS
  30. def handle(self, tok, context):
  31. if tok not in Tokens.TOKENS:
  32. return self.handle_variable(tok, context)
  33. elif tok == Tokens.OPEN:
  34. return self.handle_open(tok, context)
  35. def get_BooleanExpression_factory(self, tok):
  36. if tok == Tokens.IMP:
  37. return ImpExpression
  38. else:
  39. return None
  40. def make_BooleanExpression(self, factory, first, second):
  41. return factory(first, second)
  42. def attempt_ApplicationExpression(self, expression, context):
  43. """Attempt to make an application expression. If the next tokens
  44. are an argument in parens, then the argument expression is a
  45. function being applied to the arguments. Otherwise, return the
  46. argument expression."""
  47. if self.has_priority(APP, context):
  48. if self.inRange(0) and self.token(0) == Tokens.OPEN:
  49. self.token() # swallow then open paren
  50. argument = self.process_next_expression(APP)
  51. self.assertNextToken(Tokens.CLOSE)
  52. expression = ApplicationExpression(expression, argument, None)
  53. return expression
  54. def make_VariableExpression(self, name):
  55. if name[0].isupper():
  56. return VariableExpression(name)
  57. else:
  58. return ConstantExpression(name)
  59. @python_2_unicode_compatible
  60. class Expression(object):
  61. _linear_logic_parser = LinearLogicParser()
  62. @classmethod
  63. def fromstring(cls, s):
  64. return cls._linear_logic_parser.parse(s)
  65. def applyto(self, other, other_indices=None):
  66. return ApplicationExpression(self, other, other_indices)
  67. def __call__(self, other):
  68. return self.applyto(other)
  69. def __repr__(self):
  70. return '<%s %s>' % (self.__class__.__name__, self)
  71. @python_2_unicode_compatible
  72. class AtomicExpression(Expression):
  73. def __init__(self, name, dependencies=None):
  74. """
  75. :param name: str for the constant name
  76. :param dependencies: list of int for the indices on which this atom is dependent
  77. """
  78. assert isinstance(name, string_types)
  79. self.name = name
  80. if not dependencies:
  81. dependencies = []
  82. self.dependencies = dependencies
  83. def simplify(self, bindings=None):
  84. """
  85. If 'self' is bound by 'bindings', return the atomic to which it is bound.
  86. Otherwise, return self.
  87. :param bindings: ``BindingDict`` A dictionary of bindings used to simplify
  88. :return: ``AtomicExpression``
  89. """
  90. if bindings and self in bindings:
  91. return bindings[self]
  92. else:
  93. return self
  94. def compile_pos(self, index_counter, glueFormulaFactory):
  95. """
  96. From Iddo Lev's PhD Dissertation p108-109
  97. :param index_counter: ``Counter`` for unique indices
  98. :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas
  99. :return: (``Expression``,set) for the compiled linear logic and any newly created glue formulas
  100. """
  101. self.dependencies = []
  102. return (self, [])
  103. def compile_neg(self, index_counter, glueFormulaFactory):
  104. """
  105. From Iddo Lev's PhD Dissertation p108-109
  106. :param index_counter: ``Counter`` for unique indices
  107. :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas
  108. :return: (``Expression``,set) for the compiled linear logic and any newly created glue formulas
  109. """
  110. self.dependencies = []
  111. return (self, [])
  112. def initialize_labels(self, fstruct):
  113. self.name = fstruct.initialize_label(self.name.lower())
  114. def __eq__(self, other):
  115. return self.__class__ == other.__class__ and self.name == other.name
  116. def __ne__(self, other):
  117. return not self == other
  118. def __str__(self):
  119. accum = self.name
  120. if self.dependencies:
  121. accum += "%s" % self.dependencies
  122. return accum
  123. def __hash__(self):
  124. return hash(self.name)
  125. class ConstantExpression(AtomicExpression):
  126. def unify(self, other, bindings):
  127. """
  128. If 'other' is a constant, then it must be equal to 'self'. If 'other' is a variable,
  129. then it must not be bound to anything other than 'self'.
  130. :param other: ``Expression``
  131. :param bindings: ``BindingDict`` A dictionary of all current bindings
  132. :return: ``BindingDict`` A new combined dictionary of of 'bindings' and any new binding
  133. :raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings'
  134. """
  135. assert isinstance(other, Expression)
  136. if isinstance(other, VariableExpression):
  137. try:
  138. return bindings + BindingDict([(other, self)])
  139. except VariableBindingException:
  140. pass
  141. elif self == other:
  142. return bindings
  143. raise UnificationException(self, other, bindings)
  144. class VariableExpression(AtomicExpression):
  145. def unify(self, other, bindings):
  146. """
  147. 'self' must not be bound to anything other than 'other'.
  148. :param other: ``Expression``
  149. :param bindings: ``BindingDict`` A dictionary of all current bindings
  150. :return: ``BindingDict`` A new combined dictionary of of 'bindings' and the new binding
  151. :raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings'
  152. """
  153. assert isinstance(other, Expression)
  154. try:
  155. if self == other:
  156. return bindings
  157. else:
  158. return bindings + BindingDict([(self, other)])
  159. except VariableBindingException:
  160. raise UnificationException(self, other, bindings)
  161. @python_2_unicode_compatible
  162. class ImpExpression(Expression):
  163. def __init__(self, antecedent, consequent):
  164. """
  165. :param antecedent: ``Expression`` for the antecedent
  166. :param consequent: ``Expression`` for the consequent
  167. """
  168. assert isinstance(antecedent, Expression)
  169. assert isinstance(consequent, Expression)
  170. self.antecedent = antecedent
  171. self.consequent = consequent
  172. def simplify(self, bindings=None):
  173. return self.__class__(
  174. self.antecedent.simplify(bindings), self.consequent.simplify(bindings)
  175. )
  176. def unify(self, other, bindings):
  177. """
  178. Both the antecedent and consequent of 'self' and 'other' must unify.
  179. :param other: ``ImpExpression``
  180. :param bindings: ``BindingDict`` A dictionary of all current bindings
  181. :return: ``BindingDict`` A new combined dictionary of of 'bindings' and any new bindings
  182. :raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings'
  183. """
  184. assert isinstance(other, ImpExpression)
  185. try:
  186. return (
  187. bindings
  188. + self.antecedent.unify(other.antecedent, bindings)
  189. + self.consequent.unify(other.consequent, bindings)
  190. )
  191. except VariableBindingException:
  192. raise UnificationException(self, other, bindings)
  193. def compile_pos(self, index_counter, glueFormulaFactory):
  194. """
  195. From Iddo Lev's PhD Dissertation p108-109
  196. :param index_counter: ``Counter`` for unique indices
  197. :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas
  198. :return: (``Expression``,set) for the compiled linear logic and any newly created glue formulas
  199. """
  200. (a, a_new) = self.antecedent.compile_neg(index_counter, glueFormulaFactory)
  201. (c, c_new) = self.consequent.compile_pos(index_counter, glueFormulaFactory)
  202. return (ImpExpression(a, c), a_new + c_new)
  203. def compile_neg(self, index_counter, glueFormulaFactory):
  204. """
  205. From Iddo Lev's PhD Dissertation p108-109
  206. :param index_counter: ``Counter`` for unique indices
  207. :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas
  208. :return: (``Expression``,list of ``GlueFormula``) for the compiled linear logic and any newly created glue formulas
  209. """
  210. (a, a_new) = self.antecedent.compile_pos(index_counter, glueFormulaFactory)
  211. (c, c_new) = self.consequent.compile_neg(index_counter, glueFormulaFactory)
  212. fresh_index = index_counter.get()
  213. c.dependencies.append(fresh_index)
  214. new_v = glueFormulaFactory('v%s' % fresh_index, a, set([fresh_index]))
  215. return (c, a_new + c_new + [new_v])
  216. def initialize_labels(self, fstruct):
  217. self.antecedent.initialize_labels(fstruct)
  218. self.consequent.initialize_labels(fstruct)
  219. def __eq__(self, other):
  220. return (
  221. self.__class__ == other.__class__
  222. and self.antecedent == other.antecedent
  223. and self.consequent == other.consequent
  224. )
  225. def __ne__(self, other):
  226. return not self == other
  227. def __str__(self):
  228. return "%s%s %s %s%s" % (
  229. Tokens.OPEN,
  230. self.antecedent,
  231. Tokens.IMP,
  232. self.consequent,
  233. Tokens.CLOSE,
  234. )
  235. def __hash__(self):
  236. return hash(
  237. '%s%s%s' % (hash(self.antecedent), Tokens.IMP, hash(self.consequent))
  238. )
  239. @python_2_unicode_compatible
  240. class ApplicationExpression(Expression):
  241. def __init__(self, function, argument, argument_indices=None):
  242. """
  243. :param function: ``Expression`` for the function
  244. :param argument: ``Expression`` for the argument
  245. :param argument_indices: set for the indices of the glue formula from which the argument came
  246. :raise LinearLogicApplicationException: If 'function' cannot be applied to 'argument' given 'argument_indices'.
  247. """
  248. function_simp = function.simplify()
  249. argument_simp = argument.simplify()
  250. assert isinstance(function_simp, ImpExpression)
  251. assert isinstance(argument_simp, Expression)
  252. bindings = BindingDict()
  253. try:
  254. if isinstance(function, ApplicationExpression):
  255. bindings += function.bindings
  256. if isinstance(argument, ApplicationExpression):
  257. bindings += argument.bindings
  258. bindings += function_simp.antecedent.unify(argument_simp, bindings)
  259. except UnificationException as e:
  260. raise LinearLogicApplicationException(
  261. 'Cannot apply %s to %s. %s' % (function_simp, argument_simp, e)
  262. )
  263. # If you are running it on complied premises, more conditions apply
  264. if argument_indices:
  265. # A.dependencies of (A -o (B -o C)) must be a proper subset of argument_indices
  266. if not set(function_simp.antecedent.dependencies) < argument_indices:
  267. raise LinearLogicApplicationException(
  268. 'Dependencies unfulfilled when attempting to apply Linear Logic formula %s to %s'
  269. % (function_simp, argument_simp)
  270. )
  271. if set(function_simp.antecedent.dependencies) == argument_indices:
  272. raise LinearLogicApplicationException(
  273. 'Dependencies not a proper subset of indices when attempting to apply Linear Logic formula %s to %s'
  274. % (function_simp, argument_simp)
  275. )
  276. self.function = function
  277. self.argument = argument
  278. self.bindings = bindings
  279. def simplify(self, bindings=None):
  280. """
  281. Since function is an implication, return its consequent. There should be
  282. no need to check that the application is valid since the checking is done
  283. by the constructor.
  284. :param bindings: ``BindingDict`` A dictionary of bindings used to simplify
  285. :return: ``Expression``
  286. """
  287. if not bindings:
  288. bindings = self.bindings
  289. return self.function.simplify(bindings).consequent
  290. def __eq__(self, other):
  291. return (
  292. self.__class__ == other.__class__
  293. and self.function == other.function
  294. and self.argument == other.argument
  295. )
  296. def __ne__(self, other):
  297. return not self == other
  298. def __str__(self):
  299. return "%s" % self.function + Tokens.OPEN + "%s" % self.argument + Tokens.CLOSE
  300. def __hash__(self):
  301. return hash(
  302. '%s%s%s' % (hash(self.antecedent), Tokens.OPEN, hash(self.consequent))
  303. )
  304. @python_2_unicode_compatible
  305. class BindingDict(object):
  306. def __init__(self, bindings=None):
  307. """
  308. :param bindings:
  309. list [(``VariableExpression``, ``AtomicExpression``)] to initialize the dictionary
  310. dict {``VariableExpression``: ``AtomicExpression``} to initialize the dictionary
  311. """
  312. self.d = {}
  313. if isinstance(bindings, dict):
  314. bindings = bindings.items()
  315. if bindings:
  316. for (v, b) in bindings:
  317. self[v] = b
  318. def __setitem__(self, variable, binding):
  319. """
  320. A binding is consistent with the dict if its variable is not already bound, OR if its
  321. variable is already bound to its argument.
  322. :param variable: ``VariableExpression`` The variable bind
  323. :param binding: ``Expression`` The expression to which 'variable' should be bound
  324. :raise VariableBindingException: If the variable cannot be bound in this dictionary
  325. """
  326. assert isinstance(variable, VariableExpression)
  327. assert isinstance(binding, Expression)
  328. assert variable != binding
  329. existing = self.d.get(variable, None)
  330. if not existing or binding == existing:
  331. self.d[variable] = binding
  332. else:
  333. raise VariableBindingException(
  334. 'Variable %s already bound to another value' % (variable)
  335. )
  336. def __getitem__(self, variable):
  337. """
  338. Return the expression to which 'variable' is bound
  339. """
  340. assert isinstance(variable, VariableExpression)
  341. intermediate = self.d[variable]
  342. while intermediate:
  343. try:
  344. intermediate = self.d[intermediate]
  345. except KeyError:
  346. return intermediate
  347. def __contains__(self, item):
  348. return item in self.d
  349. def __add__(self, other):
  350. """
  351. :param other: ``BindingDict`` The dict with which to combine self
  352. :return: ``BindingDict`` A new dict containing all the elements of both parameters
  353. :raise VariableBindingException: If the parameter dictionaries are not consistent with each other
  354. """
  355. try:
  356. combined = BindingDict()
  357. for v in self.d:
  358. combined[v] = self.d[v]
  359. for v in other.d:
  360. combined[v] = other.d[v]
  361. return combined
  362. except VariableBindingException:
  363. raise VariableBindingException(
  364. 'Attempting to add two contradicting'
  365. ' VariableBindingsLists: %s, %s' % (self, other)
  366. )
  367. def __ne__(self, other):
  368. return not self == other
  369. def __eq__(self, other):
  370. if not isinstance(other, BindingDict):
  371. raise TypeError
  372. return self.d == other.d
  373. def __str__(self):
  374. return '{' + ', '.join('%s: %s' % (v, self.d[v]) for v in self.d) + '}'
  375. def __repr__(self):
  376. return 'BindingDict: %s' % self
  377. class VariableBindingException(Exception):
  378. pass
  379. class UnificationException(Exception):
  380. def __init__(self, a, b, bindings):
  381. Exception.__init__(self, 'Cannot unify %s with %s given %s' % (a, b, bindings))
  382. class LinearLogicApplicationException(Exception):
  383. pass
  384. def demo():
  385. lexpr = Expression.fromstring
  386. print(lexpr(r'f'))
  387. print(lexpr(r'(g -o f)'))
  388. print(lexpr(r'((g -o G) -o G)'))
  389. print(lexpr(r'g -o h -o f'))
  390. print(lexpr(r'(g -o f)(g)').simplify())
  391. print(lexpr(r'(H -o f)(g)').simplify())
  392. print(lexpr(r'((g -o G) -o G)((g -o f))').simplify())
  393. print(lexpr(r'(H -o H)((g -o f))').simplify())
  394. if __name__ == '__main__':
  395. demo()