bastikr / boolean.py

Implements boolean algebra in one module.
BSD 2-Clause "Simplified" License
76 stars 34 forks source link

being able to apply expressions to boolean values #73

Open gemerden opened 7 years ago

gemerden commented 7 years ago

Maybe i missed something completely, but i could not figure out how to actually use the boolean expressions to boolean values to calculate the result, so i implemented something myself:

from operator import and_ as and_operator, or_ as or_operator
from boolean import BooleanAlgebra, AND, OR, NOT, Symbol

class Symbol_(Symbol):

    def __call__(self, **kwargs):
        return kwargs[self.obj]

class NOT_(NOT):

    def __call__(self, **kwargs):
        return not self.args[0](**kwargs)

class OR_(OR):

    def __call__(self, **kwargs):
        """ reduce is used as in e.g. OR(a, b, c, d) == OR(A, OR(b, OR(c, d)))"""
        return reduce(or_operator, (a(**kwargs) for a in self.args))

class AND_(AND):

    def __call__(self, **kwargs):
        """ reduce is used as in e.g. AND(a, b, c, d) == AND(A, AND(b, AND(c, d)))"""
        return reduce(and_operator, (a(**kwargs) for a in self.args))

class CallableBooleanAlgebra(BooleanAlgebra):

    def __init__(self, *args, **kwargs):
        super(CallableBooleanAlgebra, self).__init__(Symbol_class=Symbol_,
                                                     OR_class=OR_,
                                                     AND_class=AND_,
                                                     NOT_class=NOT_,
                                                     *args, **kwargs)

if __name__ == "__main__":

    algebra = CallableBooleanAlgebra()
    exp = algebra.parse("!(x|y&(x|!z))")

    #call expression with keyword arguments matching the symbols:
    assert  exp(x=False, y=False, z=True) == True

I have written basic tests and checked whether the original tests pass (with some basic modifications) by replacing BooleanAlgebra with CallableBooleanAlgebra (import CallableBooleanAlgebra as BooleanAlgebra).

Compliments for the tight implementation of boolean that made this relatively easy.

So my questions are:

Also comments, improvements, etc. are very welcome!

Cheers, Lars

gemerden commented 7 years ago

To add some more context, the new test i wrote are:

class TestCallability(unittest.TestCase):

    def test_and(self):
        algebra = CallableBooleanAlgebra()
        exp = algebra.parse("a&b&c")
        for a in [True, False]:
            for b in [True, False]:
                for c in [True, False]:
                    self.assertEqual(exp(a=a, b=b, c=c), a and b and c)

    def test_or(self):
        algebra = CallableBooleanAlgebra()
        exp = algebra.parse("a|b|c")
        for a in [True, False]:
            for b in [True, False]:
                for c in [True, False]:
                    self.assertEqual(exp(a=a, b=b, c=c), a or b or c)

    def test_not(self):
        algebra = CallableBooleanAlgebra()
        exp = algebra.parse("!a")
        for a in [True, False]:
            self.assertEqual(exp(a=a), not a)

    def test_symbol(self):
        algebra = CallableBooleanAlgebra()
        exp = algebra.parse("a")
        for a in [True, False]:
            self.assertEqual(exp(a=a), a)

    def test_composite(self):
        algebra = CallableBooleanAlgebra()
        exp = algebra.parse("!(a|b&(a|!c))")
        for a in [True, False]:
            for b in [True, False]:
                for c in [True, False]:
                    self.assertEqual(exp(a=a, b=b, c=c), not(a or b and (a or not c)))
pombredanne commented 7 years ago

@gemerden Thanks!

So my questions are: Is this functionality already there but did or completely miss it (making this a fun but somewhat useless exercise)?

So what you are doing is evaluate a boolean expression by passing custom True/False values for Symbols? Evaluation is missing entirely! so that would be a welcome addition.

If not, is this something to add to the baseclasses (AND, OR, NOT, Symbol) themselves (but i can imagine this complicating subclassing a bit),

I am not sure why it would complicate subclassing?

Is there any interest in adding this to the repo in some other way?

Sure. Do you mind crafting a PR with your additions?

pombredanne commented 7 years ago

Compliments for the tight implementation of boolean that made this relatively easy.

All the credits go to @bastikr !

Now what I like about your approach is that is uses the Python evaluation in a clever way and it looks like it avoids the pitfalls of mixing boolean.py's TRUE and FALSE with Python's True and False, correct?

gemerden commented 7 years ago

I am not sure why it would complicate subclassing? To keep the evaluation consistent with the other changes made in the subclass, you might have to override __call__ as well, even if you do not use evaluation (otherwise you wind up with a class that only half works, although you might just raise NotImplementedError, i guess).

Now what I like about your approach is that is uses the Python evaluation in a clever way and it looks like it avoids the pitfalls of mixing boolean.py's TRUE and FALSE with Python's True and False, correct? I don't know what the role of TRUE and FALSE is, are they used as something like a constant Symbol? In that case, the Symbol.__call__ method might (guessing here) need an extra if statement.

gemerden commented 7 years ago

I have a branch ready for a PR, but i am pretty sure i need to be a collaborator to create a branch/push/PR.

pombredanne commented 7 years ago

@gemerden no need to be a collaborator: you just fork push a branch in your forked repo and then submit a PR on that branch

gemerden commented 6 years ago

If anyone is interested in a use case for evaluations, I used a somewhat customized version of boolean.py with evaluations in one of my projects: https://github.com/gemerden/wildpath (pip install wildpath):

# '|' is the OR operator

assert WildPath("start_time|end_time").get_in(agenda) == {"start_time": "10:00", "end_time": "11:00"}

# '&' is the AND operator

assert WildPath("start_*&*_time").get_in(agenda) == {"start_time": "10:00"}

# '!' is the NOT operator:

assert WildPath("!item?").get_in({"item1": "chair", "item2": "table", "count": 2}) == {"count": 2}

# parentheses can be used to indicate precedence:

assert WildPath("!(a|b)") != WildPath("!a|b")

WildPath supports paths, as in : WildPath("route.stops.*.lat|lng").get_in(some_vehicle) would give all geo-coordinates of the route of some vehicle.

jnikula commented 6 years ago

I'm not quite sure if this is what you're after, but you can evaluate the expressions by 1) providing your own subclass of Symbol with an implementation of simplify(), 2) making sure to simplify the expressions before evaluating to boolean.

import boolean

class Foo(boolean.Symbol):
    def simplify(self):
        # Replace with a sensible conversion
        if len(self.obj) > 3:
            return self.TRUE
        else:
            return self.FALSE

algebra = boolean.BooleanAlgebra(Symbol_class=Foo)
print(bool(algebra.parse('foobar and baz', simplify=True)))
print(bool(algebra.parse('foobar or baz', simplify=True)))

Side note, I think the Expression.__bool__ method should try to simplify first before throwing an exception.

pombredanne commented 6 years ago

@jnikula as a simplification, we agreed that we should not be able to evaluate a boolean expression as a Python expression (hence why Expression.__bool__ raises an exception). The rationale is that combining the two logic systems is a bit Byzantine at times especially when you consider Python shortcutting (such as in a =1 or 0whereaends up being always1and0` may never be evaluated).

So your approach makes sense alright: in subclassing Symbol you can provide exactly how you would like this evaluation to take place from a Python point of view if you need such thing. You can also of course subclass or tweak Expression.__bool__ too.

JonGalarneau commented 4 years ago

If anyone needs this kind of functionality, be warned that @jnikula's suggestion fails for certain expressions.

If you negate a symbol (ie: 'foobar and ~baz'), then NOT.demorgan() fails during algrebra.parse.simplify():

File ".../venv/lib/python3.8/site-packages/boolean/boolean.py", line 1088, in demorgan return op.dual(*(self.__class__(arg).cancel() for arg in op.args)) TypeError: '_TRUE' object is not callable

@gemerden's solution worked very well for me.

jnikula commented 4 years ago

@JonGalarneau I'll have to take your word for it.

I ended up using Lark to implement a query language for my needs.