Embedded constraint logic programming for python written in native python, by using python's versatile native syntax. This library utilizes the subscript syntax of python to preserve closer connection with prolog and internally transforms that into a data structure that is then easily and efficiently, extensible and interpolable from native python. It's about time, I introduce SocratesPy's syntax.
This library can work in two ways but only the first one is implemented right now, and the one that's implemented is transpiling a python file(for instance testFile.py) to another file(for instance transpiledTestFile.py), and you should use this command for this [this repo's main.py] testFile.py transpiledTestFile.py
.
The second method is mre interesting and would allow for all of the compilation to be done at runtime but that still remains unimplemented.
To begin writing a knowledgebase, you must first write
with rules in SocraticSolver:
where SocraticSolver
is always constant and used to denote that this is a beginning of a new knowledgebase and rules is the variable to which this knowledgebase is assigned to. After this, you'd want to add your rules to your knowledgebase, and here's how you'd do that, let's now begin to examine the syntax of rules, facts, and atoms.
with rules in SocraticSolver:
human[socrates]
human[plato]
So, now you could query this knowledgebase like this.
for _, rule in rules.lookup(SocraticQuery(human[socrates])):
print(rule)
Every iteration yields a unifier(more on that later) and a matched rule. This rule in this case yields just a human[socrates]
. Now let's examine a slightly more advance knowledgebase.
with rules in SocraticSolver:
human[socrates]
human[plato]
mortal[X] = human[X]
Here mortal[X] = human[X]
means everything that is a human is a mortal. Now you could query
for _, rule in rules.lookup(SocraticQuery(mortal[socrates])):
print(rule)
which would yield mortal[socrates]
but what happens when you query
for _, rule in rules.lookup(SocraticQuery(mortal[A])):
print(rule)
is more interesting, since it would now print
mortal(socrates)
mortal(plato)
Since this is not a tutorial for logic programming, I would dive straight into conjunctions.
fish[tuna]
fish[salmon]
fish[platypus]
mammal[elephant]
mammal[platypus]
mammal[whale]
weird[X] = mammal[X] & fish[X]
Here the &
denotes conjunction meaning both of these things must be true, here it means that X is weird if and only if X is a mammal and a fish. Now you could query something like this
for unifier, rule in rules.lookup(SocraticQuery(weird[Z])):
print(rule)
which yields weird[platypus]
. At this point we have covered most of the syntax and more examples would easily be found in testFile.py
.
This library features a pure python constraint solver with constraint propagation and backtracking that currently only provides constraints on integers but not on lists. This constraint solver only works in finite domains and numbers by default are restricted to 0..255 but soon enough there will be an option to change that. It is important to note that search is automatically performed if constraints alone fail at providing a complete solution. Here's how you'd use it to find all numbers that, when multiplied results 50 in certain bounds.
multiplyTo50[A, B] = (A > 0) & (B > 0) & (A*B == 50)
Which looked up using the ususal methods would yield
multiplyTo50(1, 50)
multiplyTo50(2, 25)
multiplyTo50(5, 10)
multiplyTo50(10, 5)
multiplyTo50(25, 2)
multiplyTo50(50, 1)
There exist more sophisticated examples that exist in testFile.py
. Note, that the only operators available right now are +
, -
, *
, /
, >
, ==
, <
, <=
and >=
.
The unifier is the object that contains the substitutions that when applied would transform your original query into your result. You can see this information as a bunch of equations like this
utils.dict_as_eqs(Substitutions.optionally_resolve(unifier.env))
utils.dict_as_eqs
is a utility function that represents a dictionary as a bunch of equations, for instance it would, given the dictionary {a: 5, x: 12}
return a = 5, x = 12
. Here Substitutions
class having the method optionally_resolve
would resolve this unifier's environment(which is where the actual relations and data is stored) but does not raise an exception when a unbounded relation is encountered.
Getting defined variables is also easy
unifier.env.get_variable("Z")
which with the aforementioned query and knowledgebase, would return platypus
. Again a few examples can be seen in testFile.py
.