Logic programming can reduce your code size, and make deductions based on facts. I’ll illustrate this using pyDatalog, a Datalog implementation in Python 3, to create a unit conversion program. I provide it wil some basic facts about how many meters, miles and feet there are in an inch, together with two conversion rules. Using those rules, I can use Datalog to “automagically” convert between any two units, even though I haven’t explicitly told it how to perform the conversion.
Let’s start with the completed code first:
from pyDatalog import pyDatalog from pyDatalog.pyDatalog import create_terms as terms from pyDatalog.pyDatalog import ask pyDatalog.create_terms('scale') # the long way of doing it terms('A, B, C, V') scale['meter', 'inch'] = 39.3700787 scale['mile', 'inch'] = 63360.0 scale['feet', 'inch'] = 12.0 scale[A, B] = 1/scale[B, A] scale[A,B] = scale[A,C] * scale[C, B] print(scale['inch', 'meter'] == V) print(scale['mile', 'meter'] == V) terms('conv') conv[V, A, B] = V * scale[A, B] print(conv[3, 'mile', 'meter'] == V) print(conv[1, 'meter', 'feet'] == V)
There is a division bug in pyDatalog v 0.14.6, so if you want to run the example, you will need to check out changeset b1a5df9 until a new version is released.
Now let me walk through the code. First, I import the pyDatalog module, and define some convenience functions:
from pyDatalog import pyDatalog from pyDatalog.pyDatalog import create_terms as terms from pyDatalog.pyDatalog import ask
Next, let’s create some terms: scale, which is a function (actually a term) where we specify how one unit relates to another, and A,B,C, V, which we think of as matching “variables”:
pyDatalog.create_terms('scale') # the long way of doing it terms('A, B, C, V')
Then, let use give some facts about how to scale between various units:
scale['meter', 'inch'] = 39.3700787 scale['mile', 'inch'] = 63360.0 scale['feet', 'inch'] = 12.0
So, these facts state that 1 meter has 39.3700787 inches, and so on. They look like functions, but they are in fact terms:
In [3]: type(scale) Out[3]: pyDatalog.pyParser.Term
The same applies for A, B, C, and V. So far, so good. But what if we want to convert, say, inches into meters instead of meters into inches? Then we need an inverse rule:
scale[A, B] = 1/scale[B, A]
Let’s test it:
In [5]: print(scale['inch', 'meter'] == V) V ----------------- 0.025400000025908
Datalog has used the inverse rule to “go the other way”. There is some minor magic going on here. Note that we didn’t specify which fact to use for inversion, Datalog figured it out for itself.
Now let’s cast some real magic. Specify a transition rule:
scale[A,B] = scale[A,C] * scale[C, B]
which states that you can get from unit A to B if there exists another unit C that serves as an intermediate conversion. Now test it:
In [6]: print(scale['mile', 'meter'] == V) V ------------------ 1609.3440016415307
Good. It’s cleverness doesn’t stop there:
In [7]: scale['league', 'mile'] = 3.45233834 In [8]: print(scale['league', 'meter'] == V) V ----------------- 5555.999999116079
Notice how there is no simple way to convert from leagues to meters. Datalog must deduce a chain of transitions in order to obtain the result.
To round things off, let’s define a convenience function, “conv”, which converts V units in A into units of B:
conv[V, A, B] = V * scale[A, B]
Perhaps we could have used a simple python function for that, but let’s keep with the paradigm. We can then ask questions like: what is 3 miles in meters:
In [9]: print(conv[3, 'mile', 'meter'] == V) V ----------------- 4828.032004924591
Bon apetitite.
With version 0.16.0, it is necessary to re-order the definition of scale as follows, in order to avoid an infinite loop :
scale[A,B] = scale[A,C] * scale[C, B]
scale[A, B] = 1/scale[B, A]
As the tutorial says : “The most general definition of the function is given first. When searching for possible answers, pyDatalog begins with the last rule defined, i.e. the more specific, and stops as soon as a valid answer is found for the function. ”
In this case, the engine should start to resolve a query with scale[A, B] = 1/scale[B, A], not with scale[A,B] = scale[A,C] * scale[C, B]