# # 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()