Welcome, guest | Sign In | My Account | Store | Cart

Some of Python's powerful meta-programming features are used to enable writing Python functions which include Prolog-like statements. Such functions load a Prolog-like database. When coupled with a suitable inference engine for logic databases, this is a way to add logical programming -- the last unsupported major paradigm -- to Python. Start at the bottom of the code for an example of the enabled syntax.

Python, 176 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#
# Pythologic.py
#
# Add logic programming (Prolog) syntax into Python.
#
# (c) 2004 Shai Berger
#
import string

class Struct:
    def __init__(self, database, head, subs):
        """
        The head and subs are essential - what makes this struct.
        The database should only be used while structs are constructed,
        and later removed.
        """
        self.database = database
        self.head = head
        self.subs = subs

    def __pos__(self):
        """
        unary + means insert into database as fact
        """
        self.database.add_fact(self)

    def __lshift__(self, requisites):
        """
        The ideal is
        consequent(args) << cond1(args1),...
        for now we must do with
        consequent(args) << (cond1(args1),...)
        """
        self.database.add_conditional(self, requisites)

    def __str__(self):
        subs = map (str, self.subs)
        return str(self.head) + "(" + string.join(subs,',') + ")"

class Symbol:
    def __init__ (self, name, database):
        self.name = name
        self.database = database
    def __call__ (self, *args):
        return Struct(self.database, self, args)
    def __str__(self):
        return self.name

class Constant(Symbol):
    """
    A constant is a name. Its value is its name too.
    """
    def value(self): return self.name

class Variable(Symbol):
    def __str__(self):
        return "?"+self.name


def symbol(name, database):
    if (name[0] in string.uppercase):
        return Variable(name,database)
    else:
        return Constant(name, database)

class Database:
    def __init__(self):
        self.facts = []
        self.conditionals = []
    def add_fact(self, fact):
        self.facts.append(fact)
    def add_conditional(self,head,requisites):
        # Older Python
        # if not(isinstance(requisites, type([]))):
        # More modern
        if not(isinstance(requisites, list)):
            requisites = [requisites]
        self.conditionals.append((head,requisites))

    def prt(self):
        """
        Print the database in somewhat readable (prolog) form
        """
        for f in self.facts: print f, "."
        for (h,r) in self.conditionals:
            print h, ":-", string.join(map(str,r), " , "), "."

    def consult(self, func):
        """
        Include definitions from func into database
        """
        try:
            code = func.func_code
        except:
            raise TypeError, "function or method argument expected"
        names = code.co_names
        locally_defined = code.co_varnames
        globally_defined = func.func_globals.keys()
        defined = locally_defined+tuple(globally_defined)
        # Python < 2.0
        # undefined = filter (lambda n,d=defined: n not in d, names)
        # Modern Python
        undefined = [name for name in names if name not in defined]
        # Generate the new global environment for the function;
        # to the old environment, add definitions for all undefined
        # symbols, which relate to this database (self). When the
        # symbols are operated on in the function, they will add
        # facts and conditionals to the database.
        newglobals = func.func_globals.copy()
        for name in undefined:
            newglobals[name] = symbol(name, self)
        exec code in newglobals

    def consult_and_transform(self, func):
        """
        A helper for decorator implementation
        """
        self.consult(func)
        return LogicalFunction(self, func)

class LogicalFunction:
    """
    This class replaces a logical function once it has
    been consulted, to avoid erroneous use
    """
    def __init__(self, database, func):
        self.database=database
        self.logical_function=func
    def __call__(self):
        raise TypeError, "Logical functions are not really callable"

def logical(database):
    """
    A decorator for logical functions
    """
    return database.consult_and_transform

if __name__ == "__main__":

    db = Database()
    global_var = ["known", "fact"]

    print "Defining a logical function...",

    @logical(db)
    def prolog_func():
        # Undefined names are given logical meaning.
        #
        # Following Prolog, if the name starts with an uppercase letter,
        # it is a logical variable (will be printed with a prefixed "?"
        # to clarify), otherwise it is a logical constant.
        #
        # unary plus defines a fact
        + farmer(moshe)
        + donkey(eeyore)
        # left-shift defines a conditional (this is an encoding
        # of the famous "donkey sentence" studied a lot in natural
        # language semantics: "If a farmer has a donkey, he beats it").
        beats(X,Y) << [ farmer(X), donkey(Y), owns(X,Y) ]
        # Define local variables -- regular Python
        x = "'local value of x'"; y = 17
        # Local and global variables (as well as other expressions)
        # can participate in facts and conditionals
        + globally(global_var)
        equal("x","y") << equal(x,y)

    # For Pre-2.4, replace the @logical decorator with this line:
    # prolog_func = db.consult_and_transform(prolog_func)
    print "Done."
    print "Definition has already updated the database as follows:"
    print
    db.prt()
    print
    print "Trying to call the logical function raises an error:"
    print
    prolog_func()

Python is widely acclaimed for supporting many programming paradigms; you can write procedural code, object oriented code, functional code, and thanks to metaclasses, even aspect oriented programming is not hard. However, Python has no support for the logical programming paradigm; this recipe aims to bring Python a little closer there.

Start at the bottom of the source. The goal of this exercise is to enable the writing of functions like prolog_func(), where a collection of facts and rules can be written in a language reminiscent of Prolog and First-Order Logic. These facts and rules are collected into a database, where an inference engine can later use them to answer queries (the inference engine and query interface are out of scope here, and left out).

Putting logical inference code into Python has been done before, e.g. http://christophe.delord.free.fr/en/pylog/. But Pylog makes it relatively hard to use Python objects from the Prolog code (you can, but not using Prolog syntax). This is the problem Pythologic solves.

The "magic" is divided between Database.consult(), which turns all undefined names in the function to logical symbols, and the overloaded operators in the Struct and Symbol classes.

This recipe has some problems: First of all, it is wildly unpythonic, in its abusive overhaul of the function semantics. At a more detailed level, the function is called upon definition (which would typically mean during import), which may cause surprising problems with respect to normal Python code in it. The function is called with a specially-constructed global environment, which means assignments to globals will not take effect (this is quite easy to fix, but would clutter the code somewhat). For perfect integration, Python callables appearing in Prolog rules should not be called until the rules are evaluated (and then, should be called with values obtained from logical variables). The recipe does not support this -- this would require some pretty deep code transformations, and this is a proof-of-concept only. If anyone chooses to go there, I would also suggest removing the unary plus requirement.

Still, I think it is an interesting, and (if I may say so myself) thought-provoking example of how far a little meta-programming can take you.

Although this recipe uses a @decorator, and therefore applies only to Python 2.4, it is very easy to follow instructions in the code to make it work with older Pythons; this should work even down to 1.5.2, though I haven't tested it.

11 comments

Itay Zandbank 19 years, 7 months ago  # | flag

Wow! Python kicks ass!

Andrew Durdin 19 years, 6 months ago  # | flag

I think you meant... If Python has an ass, he kicks it!

Markus Schatten 16 years, 11 months ago  # | flag

Reasonable Python. Hello everyone,

I know that this is a pretty old recipe but I just wanted to draw your attention to the Reasonable Python projects which uses some of the concepts stated above at (http://reasonablepy.sf.net). I'm trying to use F-Logic in Python which is a kind of object-oriented logic programming.

Best regards

Pierre Carbonnelle 11 years, 12 months ago  # | flag

pyDatalog uses this approach to make a Datalog engine available in python. Datalog is a subset of Prolog.

Shai Berger (author) 11 years, 11 months ago  # | flag

Hi Pierre, thanks for the compliment. I also noticed you gave me copyright in your source, which is, frankly, more than I expected.

Lately, I have spent some more time on this, trying to get full integration with Python (i.e. queries expressed as naturally as facts in Python code), and made some progress. Nothing is released yet, I promise to let you know when I have something more.

Markus, if you're still following this: I apologize for not responding in five years; haven't noticed your posting, somehow.

Thank you both,

Shai.

Pierre Carbonnelle 11 years, 11 months ago  # | flag

Hi Shai,

Thank you for your message and interest. I would also appreciate you input on the following improvement to pyDatalog : why not allow the definition of a class with a mix of traditional python code AND datalog logic clauses ?

An example would be something like this :

Class Person(datalog)
    def __init__(self, parent, birthdate):
        self.parent = parent
        self.birthdate = birthdate

    @pyDatalog.program()
    def _():
        Person.age(X,N) <= Person(X) & Person.birtdate(X,N1) & Now(t) & (N==t-n1)
        Person.ancestor(X,Y) <= Person.parent(X,Y)
        Person.ancestor(X,Y) <= Person.parent(X,Z) & Person.ancestor(Z,Y)

John= Person('', '1962-01-14')
Mary= Person('John', '1992-07-26')
Print Mary.ancestor() # prints John
Print Person.ask(Age(X) # prints the list of persons of age X

What do you think ? (I'll work on it at some point, but first I want to make installing pyDatalog a bit easier).

Shai Berger (author) 11 years, 11 months ago  # | flag

Hi Pierre,

I'm sorry to say, I think this suggestion is problematic, and on two levels.

On a syntactic level, you try to use Age in the last line in a place where you don't control the namespace; you'd have to change it to something like

print Person.ask.age(X) # prints the list of persons of age X

Likewise, you'd want a list of ancestors when you ask for them, and you'd probably want

print mary.ask.ancestor()

Also, since you execute the logical function at the end of definition, the syntax you propose for defining predicates as class attributes cannot work -- because at the point the logical function _ is called, the class Person does not yet exist. Not sure how you could remedy that easily.

Semantically, I think the attempt to marry classes and predicates is awkward; the direction I am following is almost diametrically opposed: I want to make the logical machinery as orthogonal as possible to other Python facilities, so that normal Python values can be used in rules and facts, and python expressions can be used in rules for side-effects. You might want to look at my presentation on this, http://www.platonix.com/static/presentations/shai/py/Pythologic.html (the stuff about queries and side-effects is in slides 9 and on).

Hope this helps,

Shai.

Pierre Carbonnelle 11 years, 11 months ago  # | flag

Thanks again for your comments.

I guess a key question is : do we embed datalog in python, or python in datalog ? In other words, will users of "pythologic" write their program primarily in python ("calling" logic clauses when needed), or in datalog ("calling" python methods when needed). You seem to favor the latter : am I correct ?

I would argue for the former, i.e logic clauses should be inserted in the traditional python program structure, i.e. in classes. If I wanted to do the former, I would add a python library to logic programming environments like SWI-Prolog or LBS : that would be a totally different project.

Or is your concern about 'object-oriented logic programming' ? Googling 'object-oriented logic programming' shows that this is not an oxymoron. I'm not sure where classes go in your proposal. I believe that classes are needed for large projects, as a way to organize complexity.

Let's continue to discuss this issue, because it is the most important. For the rest, my proposal was meant to convey the general idea : it still needs to be refined and tested for feasibility.

You are concerned that the class Person does not exist when it is used in a logic clause. I don't think that we need the Person class yet when executing the clause definition, because we load the logic function within a separate environment anyway. We can define the __getattr__ of our 'Person' symbol to do what we need. We would then avoid the syntax error. We need the Person class only when answering a query, but that comes much later, and, at that time, we can still insert the methods we need in the Person class, if any. (__getattr__ defined in the Datalog superclass may also be very handy) If that does not work, another option is to have the logic definition executed after the class definition, by removing its indentation.

Again, I welcome your comments. PC

Shai Berger (author) 11 years, 11 months ago  # | flag

A major point of making Pythologic an "internal DSL" is to avoid the dichotomy in your first paragraph; I want your question to make about as much sense as "do you write primarily loops within conditionals or conditionals within loops?".

What I have already done (but not published) is change the mechanism of logical functions so that they are actual functions -- that is, they aren't executed on definition, they can be called (and more than once), and they can take arguments. This means that you can have procedural generation of clauses on one hand, and logic backtracking as a flow-control mechanism on the other. The Python and Prolog can be fully integrated.

My concern with "marriage" is that you seem to be suggesting special facilities for predicates within classes; they are no longer regular predicates who happen to be in a class. They start to behave like methods, taking self as a first argument and whatnot. I am suspicious of this. Perhaps it is only because I haven't looked into object-oriented logic programming, but that's my instinct.

Thanks again, Shai.

Pierre Carbonnelle 11 years, 11 months ago  # | flag

OK. Thanks for the feedback. I'm not sure I understand fully where you want to get at, but that will come in due time, I suppose.

Pierre Carbonnelle 11 years, 9 months ago  # | flag

pyDatalog now embeds logic programming in python : python class can be defined by logic clauses, and logic clauses can refer to python objects directly, as I proposed earlier in this thread.