Skip to content

being able to apply expressions to boolean values #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
gemerden opened this issue Jul 9, 2017 · 11 comments
Open

being able to apply expressions to boolean values #73

gemerden opened this issue Jul 9, 2017 · 11 comments

Comments

@gemerden
Copy link
Contributor

gemerden commented Jul 9, 2017

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:

  • Is this functionality already there but did or completely miss it (making this a fun but somewhat useless exercise)?
  • If not, is this something to add to the baseclasses (AND, OR, NOT, Symbol) themselves (but i can imagine this complicating subclassing a bit),
  • Is there any interest in adding this to the repo in some other way?

Also comments, improvements, etc. are very welcome!

Cheers, Lars

@gemerden
Copy link
Contributor Author

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
Copy link
Collaborator

@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
Copy link
Collaborator

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
Copy link
Contributor Author

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
Copy link
Contributor Author

gemerden commented Jul 14, 2017

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
Copy link
Collaborator

pombredanne commented Jul 17, 2017

@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
Copy link
Contributor Author

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
Copy link

jnikula commented Apr 3, 2018

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
Copy link
Collaborator

@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 always1 and0` 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
Copy link

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
Copy link

jnikula commented Apr 27, 2020

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants