pyDatalog icon indicating copy to clipboard operation
pyDatalog copied to clipboard

Possible to use pyDatalog.predicate wrapped function in (inline) a rule body?

Open allComputableThings opened this issue 4 years ago • 8 comments

Hi all, I've been trying pyDatalog - seems quite nice.

I can create a pyDatalog.predicate as shown in the documentation:

from pyDatalog import pyDatalog
import pyDatalog.pyDatalog as dlog

pyDatalog.clear()

pyDatalog.create_terms('X, Y, p, f')

@pyDatalog.predicate()
def p(X,Y):
    yield (1,2)
    yield (2,3)

result = pyDatalog.ask('p(1,Y) & p(1,Y)')
print(result) # {(2,)}

But then...

body = p(1,Y) & p(1,Y)  # Fail
f(X) <= body

gets:

    body = p(1,Y) & p(1,Y)  # Fail
TypeError: unsupported operand type(s) for &: 'generator' and 'generator'

Is it only possible to use this predicate via the parser?

allComputableThings avatar Mar 02 '20 23:03 allComputableThings

Don't you have to add the arity to the function name ?:

def p2(X,Y):

pcarbonn avatar Mar 03 '20 17:03 pcarbonn

Thank. Not sure. PyDatalog seems to allow either: We can create: "p2", and reference "p", or "p2".

@pyDatalog.predicate()
def p2(X,Y):
    yield (1,2)
    yield (2,3)
print(pyDatalog.ask('p(1,Y)')) # Docs: set([(1, 2)]), .. Actually prints: {(2,)}
print(pyDatalog.ask('p2(1,Y)')) # {(2,)}
print(pyDatalog.ask('p1(1,Y)')) # correct AttributeError
print(pyDatalog.ask('p3(1,Y)')) # correct AttributeError

I'm a little confused about why this works since @pyDatalog.predicate() is implemented as:

def _predicate(func):
    arity = len(inspect.getargspec(func)[0])
    pyEngine.Python_resolvers[func.__name__ + '/' + str(arity)] = func
    return func

... and doesn't attempt to remove 2 from func.__name__.

Minor note, this actually prints: {(2,)} here , but the docs say to expect: {(1,2)} (https://sites.google.com/site/pydatalog/advanced-topics).

To answer the original point though, p(1,Y) & p(1,Y) and p2(1,Y) & p2(1,Y) both fail because pyDatalog.predicate simply returns a generator function and python says no to <gen func> & <gen func>. What could @pyDatalog.predicate return to allow inline use of p2? A Pred object? If I try:

@pyDatalog.predicate()
def p2(X,Y):
    yield (1,2)
    yield (2,3)
p2 = Pred("p2", 2)

q = p2(1,Y) & p2(1,Y)    # TypeError: 'Pred' object is not callable

mytest(X,Y) <=  q

I thought this might work because that's how 'p2' is represented internally during the string query:

image

I think pyDatalog.pyEngine.Pred is simply an internal concept (? it inherits Interned). It there an inline-mode / user-land equivalent that has __call__ defined? (which should return a Literal?)

allComputableThings avatar Mar 03 '20 17:03 allComputableThings

Updated the question -- really I'm looking to use a python-predicate resolver as a predicate in inline-rule.

allComputableThings avatar Mar 03 '20 17:03 allComputableThings

Here's a quick fix:

@pyDatalog.predicate()
def p2(X,Y):
    yield (1,2)
    yield (2,3)
pyDatalog.create_terms('p')
pyDatalog.clear()
pyDatalog.create_terms('mytest, mytest2, X, Y')

# class PredInline(object):
#     def __init__(self, predicate_name):
#         self.predicate_name = predicate_name
#
#     def __call__(self, *args):
#         return pyDatalog.pyParser.Literal.make(predicate_name=self.predicate_name, terms=args)


@pyDatalog.predicate()
def p2(X,Y):
    yield (1,2)
    yield (2,3)

# Parsed query --- works
# print(pyDatalog.ask('p(1,Y) & p(1,Y)')) # {(2,)}
# print(pyDatalog.ask('p(X,Y) & p(X,Y)')) # {(1, 2), (2, 3)}
# pyDatalog.load("""
# mytest2(X,Y) <= p(X,Y) & p(X,Y)
# """)
# print(pyDatalog.ask('mytest2(1,Y) & mytest2(1,Y)')) # {(2,)}
# print(pyDatalog.ask('mytest2(X,Y) & mytest2(X,Y)')) # {(1, 2), (2, 3)}

pyDatalog.create_terms('p')  # Make p available for inline use
#type(p)  # pyParser.Term
mytest(X,Y) <= p(X,Y) & p(X,Y)

print((mytest(X,Y)).ask()) # [(2, 3), (1, 2)]
print(Y.data)  # [3, 2]
print((mytest(1,Y)).ask()) # [(2,)]
print(Y.data)  # [2]

... so should pyDatalog.predicate return pyParser.Term("p")?

allComputableThings avatar Mar 03 '20 18:03 allComputableThings

Better:

pyDatalog.create_terms('mytest, mytest2, X, Y')  # << no p

def pypredicate(func=None):
    def _pypredicate(func):
        name = func.__name__
        pyDatalog._predicate(func)
        # Drop number suffix?
        # while name[-1].isdigit(): name =name[:-1]
        return pyParser.Term(name)

    if func is None: return _pypredicate
    else: return _pypredicate(func)

@pypredicate()
def p(X,Y):
    yield (1,2)
    yield (2,3)

mytest(X,Y) <= p(X,Y) & p(X,Y)

print((mytest(X,Y)).ask()) # [(2, 3), (1, 2)]
print(Y.data)  # [3, 2]
print((mytest(1,Y)).ask()) # [(2,)]
print(Y.data)  # [2]

allComputableThings avatar Mar 03 '20 18:03 allComputableThings

If I redefined pyDatalog.predicate this way, would you accept a PR?

allComputableThings avatar Mar 03 '20 18:03 allComputableThings

I tried to test that inline and parsed used of the predicate work the same. Almost, but I get:

assert type(pyDatalog.ask('p(1,2)')) == pyDatalog.pyParser.Answer
assert type(p(1,2).ask()) == list

... is it expected? I suspect it has more to do with the implementation of ask than my predicate decorator.

def predicate(func=None):
    def _pypredicate(func):
        name = func.__name__
        pyDatalog._predicate(func)
        # Drop number suffix?
        # while name[-1].isdigit(): name =name[:-1]
        return pyParser.Term(name)

    if func is None: return _pypredicate
    else: return _pypredicate(func)


def test_pypredicate_parsed():
    pyDatalog.clear()
    pyDatalog.create_terms('mytest, X, Y')

    @predicate()
    def p(X,Y):
        yield (1,2)
        yield (2,3)

    # Does "p(...) & ..."  and "... & p(...)" work?
    assert type(pyDatalog.ask('p(1,2)')) == pyDatalog.pyParser.Answer
    assert (pyDatalog.ask('p(1,2)')) == {()}
    assert (pyDatalog.ask('p(1,Y)')) == {(2,)}
    assert (pyDatalog.ask('p(1,Y) & p(1,Y)')) == {(2,)}
    assert (pyDatalog.ask('p(X,Y) & p(X,Y)')) == {(1, 2), (2, 3)}
    # In a rule...
    pyDatalog.load("""
    mytest(X,Y) <= p(X,Y) & p(X,Y)
    """)
    assert(pyDatalog.ask('mytest(1,2)&p(1,2)')) == {()}
    assert (pyDatalog.ask('mytest(1,Y)&p(1,2)')) == {(2,)}
    assert (pyDatalog.ask('mytest(X,Y)&p(X,Y)')) == {(1, 2), (2, 3)}
    assert type(Y.data) == list
    assert (Y.data)==[3,2]
    assert (X.data)==[2,1]

def test_pypredicate_inline():
    pyDatalog.clear()
    pyDatalog.create_terms('mytest2, X, Y')

    @predicate()
    def p(X,Y):
        yield (1,2)
        yield (2,3)

    # Does "p(...) & ..."  and "... & p(...)" work?
    assert type(p(1,2).ask()) == list
    assert (p(1,2).ask()) == [()]
    assert (p(1,Y).ask()) == [(2,)]
    assert (p(1,Y) & p(1,Y)).ask() == [(2,)]
    assert (p(X,Y) & p(X,Y)).ask() == [(2, 3), (1, 2)]
    # In a rule...
    mytest(X,Y) <= p(X,Y) & p(X,Y)

    assert type((mytest(1,2)&p(1,2)).ask()) == list
    assert (mytest(1,2)&p(1,2)).ask() == [()]
    assert (mytest(1,Y)&p(1,2)).ask() == [(2,)]
    assert X.data==[2,1]
    assert (mytest(X,Y)&p(X,Y)).ask() == [(2, 3), (1, 2)]
    assert type(Y.data) == list
    assert (Y.data)==[3,2]
    assert (X.data)==[2,1]

test_pypredicate_parsed()
test_pypredicate_inline()

allComputableThings avatar Mar 03 '20 19:03 allComputableThings

Also, should:

def load(code):
    """loads the clauses contained in the code string """
    stack = inspect.stack()
    newglobals = {}
    for key, value in stack[1][0].f_globals.items():
        if hasattr(value, '_pyD_atomized'):
            newglobals[key] = value
    return pyParser.load(code, newglobals=newglobals)

... also load locals, not just globals? I'm having problems getting it to see the effect of pyDatalog.create_terms or @predicate() if called from inside the scope of a function body.

allComputableThings avatar Mar 03 '20 19:03 allComputableThings