"""
Extensible combinator library for building symbolic expressions that
can be evaluated at a later time.
"""
from __future__ import annotations
from typing import Any, Union, Iterable
import doctest
[docs]class symbol:
"""
Instances of this class represent individual symbolic values, as well as
entire symbolic expressions (*i.e.*, trees consisting of nested :obj:`symbol`
instances are represented using the root instance). A symbolic expression
involving addition of integers is created in the example below.
>>> from symbolism import *
>>> addition = symbol(lambda x, y: x + y)
>>> summation = addition(symbol(1), symbol(2))
The expression above can be evaluated at a later time.
>>> summation.evaluate()
3
Instances are compatible with all built-in infix and prefix operators.
When an operator is applied to one or more instances, a new :obj:`symbol`
instance is created.
>>> summation = symbol(1) + symbol(2)
>>> summation.evaluate()
3
Pre-defined constants are also provided for all built-in Python operators.
>>> conjunction = and_(symbol(True), symbol(False))
>>> conjunction.evaluate()
False
"""
def __init__(self: symbol, instance: Any):
"""
Create a symbol from an instance (*e.g.*, value, object, or
function).
>>> add = lambda x, y: x + y
>>> add_ = symbol(add)
>>> add_.instance(1, 2)
3
"""
self.instance = instance
self.parameters = None
[docs] def __call__(self: symbol, *args, **kwargs) -> symbol:
"""
Allow creation of a symbolic expression via application of a
:obj:`symbol` instance to zero or more parameter expressions.
>>> add = lambda x, y: x + y
>>> add_ = symbol(add)
>>> e = add_(symbol(1), symbol(2))
>>> isinstance(e, symbol)
True
>>> len(e.parameters)
2
>>> e.parameters[0].instance
1
Keyword arguments are also supported. However, note that the keywords
are preserved only in the keys of the ``parameters`` attribute (which in
this case is instantiated as a dictionary). The indexing method
:obj:`~symbol.__getitem__` and the iteration method :obj:`~symbol.__iter__`
only support positional integer indexing and (where applicable) slicing.
>>> add = lambda x, y: x + y
>>> add_ = symbol(add)
>>> e = add_(x=symbol(1), y=symbol(2))
>>> isinstance(e, symbol)
True
>>> len(e.parameters)
2
>>> e.parameters['x'].instance
1
Positional and keyword arguments cannot be mixed.
>>> add = lambda x, y: x + y
>>> add_ = symbol(add)
>>> e = add_(symbol(1), y=symbol(2))
Traceback (most recent call last):
...
ValueError: cannot mix positional and keyword arguments
"""
if len(args) > 0 and len(kwargs) > 0:
raise ValueError('cannot mix positional and keyword arguments')
s = symbol(self.instance)
s.parameters = args if len(kwargs) == 0 else kwargs
return s
[docs] def __getitem__(self: symbol, key: Union[int, slice]) -> Union[Any, list, tuple]:
"""
Retrieve an instance parameter using an integer index, or retrieve a
sequence of instance parameters using a slice.
>>> add = lambda x, y: x + y
>>> add_ = symbol(add)
>>> e = add_(symbol(1), symbol(2))
>>> (e[0].instance, e[1].instance)
(1, 2)
>>> [e[i].instance for (i, p) in enumerate(e.parameters)]
[1, 2]
>>> [e[i].instance for (i, p) in enumerate(e)]
[1, 2]
Slice notation is also supported when the ``parameters``
attribute supports it.
>>> [s.instance for s in e[0:2]]
[1, 2]
"""
return (
list(self.parameters.values())[key] # pylint: disable=no-member
if isinstance(self.parameters, dict) else
self.parameters[key] # Should be a :obj:`tuple`; see :obj:`__init__`.
)
[docs] def __iter__(self: symbol) -> Iterable:
"""
Allow iteration over instance parameters.
>>> add = lambda x, y: x + y
>>> add_ = symbol(add)
>>> e = add_(symbol(1), symbol(2))
>>> [p.instance for p in e]
[1, 2]
>>> 123 in add_(123)
True
Even if keyword arguments are used when this instance is instantiated,
the iteration returns the actual parameter instances (and **not** the
keys of the ``parameters`` attribute).
>>> add = lambda x, y: x + y
>>> add_ = symbol(add)
>>> e = add_(x=symbol(1), y=symbol(2))
>>> [p.instance for p in e]
[1, 2]
>>> 123 in add_(123)
True
"""
for parameter in (
self.parameters.values() # pylint: disable=no-member
if isinstance(self.parameters, dict) else
self.parameters
):
yield parameter
[docs] def __len__(self: symbol) -> int:
"""
The length of an instance corresponds to the number of parameters that
it has.
>>> add = lambda x, y: x + y
>>> len(symbol(add))
0
>>> e = symbol(add)(symbol(1), symbol(2))
>>> len(e)
2
"""
return len(self.parameters) if self.parameters is not None else 0
[docs] def evaluate(self: symbol) -> Any:
"""
Evaluate a symbolic expression (via recursive evaluation of all
subexpressions) and return the result.
>>> add = lambda x, y: x + y
>>> e = symbol(add)(symbol(1), symbol(2))
>>> e.evaluate()
3
>>> e = symbol(list.__getitem__)(symbol(['a', 'b', 'c']), symbol(1))
>>> e.evaluate()
'b'
"""
if self.parameters is None:
return self.instance
return self.instance(*[
parameter.evaluate()
for parameter in (
self.parameters.values() # pylint: disable=no-member
if isinstance(self.parameters, dict) else
self.parameters
)
])
[docs] def __add__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) + symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
5
"""
return add_(self, other)
[docs] def __sub__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) - symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
-1
"""
return sub_(self, other)
[docs] def __mul__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) * symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
6
"""
return mul_(self, other)
[docs] def __matmul__(self: symbol, other: symbol) -> symbol:
"""
>>> class Test:
... def __matmul__(self, other):
... return True
>>> e = symbol(Test()) @ symbol(Test())
>>> isinstance(e, symbol)
True
>>> e.evaluate()
True
"""
return matmul_(self, other)
[docs] def __truediv__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(5) / symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
2.5
"""
return div_(self, other)
[docs] def __floordiv__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(5) // symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
2
"""
return floordiv_(self, other)
[docs] def __mod__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(5) % symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
1
"""
return mod_(self, other)
[docs] def __pow__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(5) ** symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
25
"""
return pow_(self, other)
[docs] def __lshift__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(4) << symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
16
"""
return lshift_(self, other)
[docs] def __rshift__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(16) >> symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
4
"""
return rshift_(self, other)
[docs] def __and__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol({1, 2}) & symbol({2, 3})
>>> isinstance(e, symbol)
True
>>> e.evaluate()
{2}
"""
return bitand_(self, other)
[docs] def __xor__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol({1, 2}) ^ symbol({2, 3})
>>> isinstance(e, symbol)
True
>>> e.evaluate()
{1, 3}
"""
return bitxor_(self, other)
[docs] def __or__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol({1, 2}) | symbol({2, 3})
>>> isinstance(e, symbol)
True
>>> e.evaluate()
{1, 2, 3}
"""
return bitor_(self, other)
[docs] def __neg__(self: symbol) -> symbol:
"""
>>> e = -symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
-2
"""
return neg_(self)
[docs] def __pos__(self: symbol) -> symbol:
"""
>>> e = +symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
2
"""
return pos_(self)
[docs] def __invert__(self: symbol) -> symbol:
"""
>>> e = ~symbol(2)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
-3
"""
return invert_(self)
[docs] def __eq__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) == symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
False
"""
return eq_(self, other)
[docs] def __ne__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) != symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
True
"""
return ne_(self, other)
[docs] def __lt__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) < symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
True
"""
return lt_(self, other)
[docs] def __le__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) <= symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
True
"""
return le_(self, other)
[docs] def __gt__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) > symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
False
"""
return gt_(self, other)
[docs] def __ge__(self: symbol, other: symbol) -> symbol:
"""
>>> e = symbol(2) >= symbol(3)
>>> isinstance(e, symbol)
True
>>> e.evaluate()
False
"""
return ge_(self, other)
# In order to accommodate the limitations of measuring coverage of unit tests,
# the @symbol decorator is not used in the definitions below.
def _and_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(and_(symbol(False), symbol(True)), symbol)
True
>>> and_.instance(True, False)
False
"""
return x and y
and_ = symbol(_and_)
"""
.. |and| replace:: ``and``
.. _and: https://docs.python.org/3/reference/expressions.html#boolean-operations
Symbolic function corresponding to the infix boolean operator |and|_.
"""
def _or_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(or_(symbol(False), symbol(True)), symbol)
True
>>> or_.instance(True, False)
True
"""
return x or y
or_ = symbol(_or_)
"""
.. |or| replace:: ``or``
.. _or: https://docs.python.org/3/reference/expressions.html#boolean-operations
Symbolic function corresponding to the infix boolean operator |or|_.
"""
def _not_(x: symbol) -> symbol:
"""
>>> isinstance(not_(symbol(True)), symbol)
True
>>> not_.instance(True)
False
"""
return not x
not_ = symbol(_not_)
"""
.. |not| replace:: ``not``
.. _not: https://docs.python.org/3/reference/expressions.html#boolean-operations
Symbolic function corresponding to the prefix boolean operator |not|_.
"""
def _in_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(in_(symbol(2), symbol({1, 2, 3})), symbol)
True
>>> in_.instance(2, {1, 2, 3})
True
"""
return x in y
in_ = symbol(_in_)
"""
.. |in| replace:: ``in``
.. _in: https://docs.python.org/3/reference/expressions.html#comparisons
Symbolic function corresponding to the infix operator |in|_.
"""
def _is_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(is_(symbol(2), symbol(2)), symbol)
True
>>> is_.instance(2, 2)
True
"""
return x is y
is_ = symbol(_is_)
"""
.. |is| replace:: ``is``
.. _is: https://docs.python.org/3/reference/expressions.html#comparisons
Symbolic function corresponding to the infix operator |is|_.
"""
def _add_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(add_(symbol(2), symbol(3)), symbol)
True
>>> add_.instance(2, 3)
5
"""
return x + y
add_ = symbol(_add_)
"""Alias for :obj:`symbol.__add__`."""
def _sub_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(sub_(symbol(2), symbol(3)), symbol)
True
>>> sub_.instance(2, 3)
-1
"""
return x - y
sub_ = symbol(_sub_)
"""Alias for :obj:`symbol.__sub__`."""
def _mul_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(mul_(symbol(2), symbol(3)), symbol)
True
>>> mul_.instance(2, 3)
6
"""
return x * y
mul_ = symbol(_mul_)
"""Alias for :obj:`symbol.__mul__`."""
def _matmul_(x: symbol, y: symbol) -> symbol:
"""
>>> class Test:
... def __matmul__(self, other):
... return True
>>> isinstance(matmul_(symbol(Test()), symbol(Test())), symbol)
True
>>> matmul_.instance(Test(), Test())
True
"""
return x @ y
matmul_ = symbol(_matmul_)
"""Alias for :obj:`symbol.__matmul__`."""
def _div_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(div_(symbol(5), symbol(2)), symbol)
True
>>> div_.instance(5, 2)
2.5
"""
return x / y
truediv_ = symbol(_div_)
"""Alias for :obj:`symbol.__truediv__`."""
div_ = truediv_
"""Concise alias for :obj:`symbol.__truediv__`."""
def _floordiv_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(floordiv_(symbol(5), symbol(2)), symbol)
True
>>> floordiv_.instance(5, 2)
2
"""
return x // y
floordiv_ = symbol(_floordiv_)
"""Alias for :obj:`symbol.__floordiv__`."""
def _mod_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(mod_(symbol(5), symbol(2)), symbol)
True
>>> mod_.instance(5, 2)
1
"""
return x % y
mod_ = symbol(_mod_)
"""Alias for :obj:`symbol.__mod__`."""
def _pow_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(pow_(symbol(2), symbol(3)), symbol)
True
>>> pow_.instance(2, 3)
8
"""
return x ** y
pow_ = symbol(_pow_)
"""Alias for :obj:`symbol.__pow__`."""
def _lshift_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(lshift_(symbol(4), symbol(2)), symbol)
True
>>> lshift_.instance(4, 2)
16
"""
return x << y
lshift_ = symbol(_lshift_)
"""Alias for :obj:`symbol.__lshift__`."""
def _rshift_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(rshift_(symbol(16), symbol(2)), symbol)
True
>>> rshift_.instance(16, 2)
4
"""
return x >> y
rshift_ = symbol(_rshift_)
"""Alias for :obj:`symbol.__rshift__`."""
def _bitand_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(bitand_(symbol({1, 2}), symbol({2, 3})), symbol)
True
>>> bitand_.instance({1, 2}, {2, 3})
{2}
"""
return x & y
bitand_ = symbol(_bitand_)
"""Alias for :obj:`symbol.__and__`."""
amp_ = bitand_
"""Concise alias for :obj:`symbol.__and__`."""
def _bitxor_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(bitxor_(symbol({1, 2}), symbol({2, 3})), symbol)
True
>>> bitxor_.instance({1, 2}, {2, 3})
{1, 3}
"""
return x ^ y
bitxor_ = symbol(_bitxor_)
"""Alias for :obj:`symbol.__xor__`."""
xor_ = bitxor_ # Concise synonym
"""Concise alias for :obj:`symbol.__xor__`."""
def _bitor_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(bitor_(symbol({1, 2}), symbol({2, 3})), symbol)
True
>>> bitor_.instance({1, 2}, {2, 3})
{1, 2, 3}
"""
return x | y
bitor_ = symbol(_bitor_)
"""Alias for :obj:`symbol.__or__`."""
bar_ = bitor_
"""Concise alias for :obj:`symbol.__or__`."""
def _invert_(x: symbol) -> symbol:
"""
>>> isinstance(invert_(symbol(2)), symbol)
True
>>> invert_.instance(2)
-3
"""
return ~x
invert_ = symbol(_invert_)
"""Alias for :obj:`symbol.__invert__`."""
def _pos_(x: symbol) -> symbol:
"""
>>> isinstance(pos_(symbol(3)), symbol)
True
>>> pos_.instance(3)
3
"""
return +x
pos_ = symbol(_pos_)
"""Alias for :obj:`symbol.__pos__`."""
uadd_ = pos_
"""Alias for :obj:`symbol.__pos__` (alluding to the name of :obj:`ast.UAdd`)."""
def _neg_(x: symbol) -> symbol:
"""
>>> isinstance(neg_(symbol(3)), symbol)
True
>>> neg_.instance(3)
-3
"""
return -x
neg_ = symbol(_neg_)
"""Alias for :obj:`symbol.__neg__`."""
usub_ = neg_
"""Alias for :obj:`symbol.__neg__` (alluding to the name of :obj:`ast.USub`)."""
def _eq_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(eq_(symbol(3), symbol(2)), symbol)
True
>>> eq_.instance(3, 2)
False
"""
return x == y
eq_ = symbol(_eq_)
"""Alias for :obj:`symbol.__eq__`."""
def _ne_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(ne_(symbol(3), symbol(2)), symbol)
True
>>> ne_.instance(3, 2)
True
"""
return x != y
ne_ = symbol(_ne_)
"""Alias for :obj:`symbol.__ne__`."""
def _lt_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(lt_(symbol(3), symbol(2)), symbol)
True
>>> lt_.instance(3, 2)
False
"""
return x < y
lt_ = symbol(_lt_)
"""Alias for :obj:`symbol.__lt__`."""
def _le_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(le_(symbol(3), symbol(2)), symbol)
True
>>> le_.instance(3, 2)
False
"""
return x <= y
le_ = symbol(_le_)
"""Alias for :obj:`symbol.__le__`."""
def _gt_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(gt_(symbol(3), symbol(2)), symbol)
True
>>> gt_.instance(3, 2)
True
"""
return x > y
gt_ = symbol(_gt_)
"""Alias for :obj:`symbol.__gt__`."""
def _ge_(x: symbol, y: symbol) -> symbol:
"""
>>> isinstance(ge_(symbol(3), symbol(2)), symbol)
True
>>> ge_.instance(3, 2)
True
"""
return x >= y
ge_ = symbol(_ge_)
"""Alias for :obj:`symbol.__ge__`."""
if __name__ == '__main__':
doctest.testmod() # pragma: no cover