Category Archives: Prolog

[repost ]How to create a standalone from a SICStus prolog file

original:http://www.csc.kth.se/~jwikst/sicstus_standalone.html

After having searched the internet, I e-mailed SICS and got two methods to create a standalone. I was using:

  • SICStus prolog VC9 4.2.0
  • Microsoft Visual C++ 2010 Service Pack 1

To create a standalone .exe file you need a C-compiler that matches your installation of SICStus. These C-compilers are included in the following versions of Visual C++.

  • SICStus VC10 4.2.x – Visual Studio 2010 SP1
  • SICStus VC9 4.2.x – Visual Studio 2008 SP1
  • SICStus [without VC] – Visual Studio 2005 SP1

Although there seems to be some backwards compability since I’m using SICStus VC9 and Visual Studio 2010.

To run the .exe file on another computer that computer will need the corresponding C-library(which is called Redistributable Packages by Microsoft). They can be downloaded here:

  • SICStus VC10 4.2.x – Visual C++ 2010 SP1 Redistributable Package (x86)
  • SICStus VC9 4.2.x – Visual C++ 2008 SP1 Redistributable Package (x86)
  • SICStus [without VC] – Visual C++ 2005 SP1 Redistributable Package (x86)

This is the example pl file we will be compiling. It’s using one of SICStus built-in libraries namely library(sets). Lets call it test.pl

:- module(test, []).
:- use_module(library(sets), [intersection/3]).

main :-

%% argv is the commands passed on the command line (or after the
%% option '--' if run in a development system).
current_prolog_flag(argv, Argv),
sort(Argv, Set1),
Set2 = [a, c, g],
intersection(Set1, Set2, Intersection),
format('The intersection of ~w and ~w is ~w~n', [Set1, Set2, Intersection]).

% .sav-file entry point
user:runtime_entry(start) :-
main.
%% test.pl

Method 1:

Create the .sav-file by compiling from the SICStus top level. This can be done using the SICStus interpreter,emacs, SPIDER or from the command line by navigating to the SICStus binary folder “C:\Program Files (x86)\SICStus Prolog VC10 4.2.1\bin” and typing:

sicstus.exe -i

From the sicstus top level, type the following:

|?- compile('C:\filepath...\test'), save_program('C:\filepath...\test.sav').

Then end your session using

|?- halt.

Now you need to load some settings to make your C-compiler available. Do this by navigating to (might be slightly different depending on your version of visual studio) C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\ run the vcvarsall file.

vcvarsall.bat x86

Remember the x86.

Then navigate to the SICStus binary folder and build your exe file using spld.exe. The flag –static makes sure that all you’ll need will be in the .exe file though the target machine must have the correct C-library installed.

C:\Program Files (x86)\SICStus Prolog VC10 4.2.1\bin>spld.exe --static c:\long filepath...\test.sav -o c:\long filepath...\test1.exe

This should create an .exe file that will run on any computer with the right C library.

METHOD 2

Create the save file again. It’s important that you name it main.sav.

|?- compile('C:\filepath...\test'), save_program('C:\filepath...\main.sav').

Create a new folder for this example. Copy your main.sav file to the folder. Copy sprt.exe, sprt4-2-x.dll and sprt.sav from the SICStus installation’s bin folder into your new folder.

To run your program, run sprt.exe. Now your folder contains everything you need to run your .exe on another computer(except for the C library).

I hope this will help someone stuck in the same situation as I was.

This is a translation of a mail conversation with Per Mildner at SICS. The entire conversation (IN SWEDISH) can be found here

[repost ]Mixing Java and Prolog

original:http://sicstus.sics.se/sicstus/docs/3.7.1/html/sicstus_12.html

Jasper is a bi-directional interface between programs written in Java and programs written in Prolog. The Java-side of the interface constists of a Java package (jasper) containing classes representing the SICStus emulator. The Prolog part is designed as an extension to the foreign language interface, which means that foreign/3 declarations are used to map Java-methods on Prolog predicates and provide automatic conversion of arguments.

Jasper can be operated in two different ways, depending on which emulator is used as top-level application; the Java Runtime System or the SICStus Development System. When the Java Runtime System is used as a top-level application, the SICStus runtime kernel is loaded into the Java Runtime System. When the Java Runtime System is loaded into the SICStus Development System, it is loaded as any other foreign resource, i.e. by using load_foreign_resource/1. This distinction is not visible to the programmer, but it is useful to have a picture of how the interface works.

Note: Some of the information in this chapter is a recapitulation of the information in the chapter section Mixing C and Prolog. The intention is that this chapter should be possible to read fairly independently.

Prerequisites

The low-level interface uses the JNI (Java Native Interface) to call C functions from Java. The JNI is a native interface standard developed by Sun Microsystems. See section Resources. Since the interface uses the JNI, it is necessary that the Java VM which is to be used has support for JNI. Even though the JNI is intended as a standard, not all vendors support it. If your Java installation does not support the JNI, Sun’s Java Runtime Environment provides a minimal execution enviroment for running Java code. It can be downloaded from

http://java.sun.com/products/jdk/1.1/jre/index.html

The rest of this chapter assumes that there is a Java installation with JNI support available.

Calling Java from Prolog

Java methods are called from Prolog much in the same way as C functions are called (see section Calling C from Prolog); by creating a foreign resource. When loaded, this resource installs a set of predicates which are mapped onto Java-methods and when invoked, converts the Prolog arguments to the corresponding Java-types before calling the Java method itself.

In fact, a foreign resource (as defined in section Foreign Resource and Conversion Declarations) is not language specific itself. The language is instead specified in the second argument to theforeign/3 fact and it is possible to mix foreign C functions with foreign Java methods.

How a foreign resource is created in general is described in detail in section Creating the Linked Foreign Resource. The following section(s) will focus on the Java-specific parts of foreign resources.

Static and Dynamic Linking

There is no support for static linking of foreign resources containing Java declarations, since Java implementations usually do not support static linking.

Declarating Java-methods

Java-methods are declared similarly to C-functions. There are two major differences. The first is how methods are identified. It is not enough to simply use an atom as the C interface does. Instead, a term method/3 is introduced:

method(+ClassName,+MethodName,+Flags)
Used as first argument to foreign/3 when declaring Java methods. The first argument is an atom containing the Fully Qualified Classname of the class. The second argument is the method name. The third argument is a list of flags. Possible flags are instance or static, indicating whether or not the method is static or non-static. Non-static methods must have an object-reference as their first argument. This is a reference to the object on which the method will be invoked. This term is then used to identify the method in the foreign_resource/2 predicate. So, to define a foreign resource exporting the non-static Java method getFactors in the class PrimeNumber in the package numbers, the method/3 term would look like

method('numbers/PrimeNumber','getFactors',[instance])

The syntax for foreign/3 is the basically the same as for C-functions:

foreign(+MethodIdentifierjava+Predicate)
hook predicate, specifies the Prolog interface to a Java method. MethodIdentifier is method/3 term as described above. Predicate specifies the name of the Prolog predicate that will be used to call MethodIdentifierPredicate also specifies how the predicate arguments are to be translated into the corresponding Java arguments.

Conversions between Prolog Arguments and Java Types

The following table lists the possible values of arguments of the predicate specification to foreign/3. The value declares which conversion between corresponding Prolog argument and Java type will take place.

Note: The conversion declarations (composed of the declarators specified below) together with the method/3 term are used by the glue-code generator to create the method’s type signature, i.e. a string which can uniquely identify a method within a class. This means that unlike the C interface, the conversion declarations for a Java method will affect the lookup of the method-name (in the C interface, only the function name is relevant). So, if a method is declared as foo(+integer), there must be a method which has the name foo and takes one argument of type int, or an argument which can be automatically converted to an int (a short, for example).

Prolog: +integer
Java: int
The argument should be a number. It is converted to a Java int.
Prolog: +byte
Java: byte
The argument should be a number. It is converted to a Java byte.
Prolog: +short
Java: short
The argument should be a number. It is converted to a Java short.
Prolog: +long
Java: long
The argument should be a number. It is converted to a Java longNote: Since Java’s long type is 64 bits wide and there is no standardized support for 64 bits integers in C, the value will be truncated. So, this declaration is really only useful in order to indicate which method should be used. For example:

class Bar
{
  void foo(int x)
    { ... }

  void foo(long x)
    { ... }
}

In order to be able to indicate that the latter of the foo methods should be called, a +long declaration must be used, even if the value itself will be truncated in the call.

Prolog: +float
Java: float
The argument should be a number. It is converted to a Java float.
Prolog: +double
Java: double
The argument should be a number. It is converted to a Java double.
Prolog: +term
Java: SPTerm
The argument can be any term. It is passed to Java as an object of the class SPTerm.
Prolog: +object(Class)
Java: SPTerm
The argument should be the Prolog representation of a Java object of class Class. Unless it is the first argument in a non-static method (in which case is it treated as the object on which the method should be invoked), it is passed to the Java method as an object of class Class.
Prolog: +atom
Java: SPTerm
The argument should be an atom. The Java method will be passed an object of class SPTerm.
Prolog: +boolean
Java: boolean
The argument should be an atom in {true,false}. The Java method will receive a boolean.
Prolog: +chars
Java: String
The argument should be a list of character codes. The Java method will receive an object of class String.
Prolog: +string
Java: String
The argument should be an atom. The Java method will receive an object of class String.
Prolog: -atom
Java: SPTerm
The argument should be an unbound variable. The Java method will receive an atom of class SPTerm which can be modified. The argument will be bound to the value of the atom when the method returns.
Prolog: -chars
Java: StringBuffer
The argument should be an unbound variable. The Java method will receive an object of type StringBuffer which can be modified. The argument will be bound to a list of the character codes of the StringBuffer object.
Prolog: -string
Java: StringBuffer
The argument should be an unbound variable. The Java method will receive an object of type StringBuffer which can be modified. The argument will be bound to an atom converted from theStringBuffer object.
Prolog: [-integer]
Java: int M()
The Java method should return an int. The value will be converted to a Prolog integer.
Prolog: [-byte]
Java: byte M()
The Java method should return a byte. The value will be converted to a Prolog integer.
Prolog: [-short]
Java: short M()
The Java method should return a short. The value will be converted to a Prolog integer.
Prolog: [-long]
Java: long M()
The Java method should return a long. The value will be converted and possibly truncated to a Prolog integer.
Prolog: [-float]
Java: float M()
The Java method should return a float. The value will be converted to a Prolog float.
Prolog: [-double]
Java: double M()
The Java method should return a double. The value will be converted to a Prolog float.
Prolog: [-term]
Java: SPTerm M()
The Java method should return an object of class SPTerm which will be converted to a Prolog term.
Prolog: [-object(Class)]
Java: SPTerm M()
The Java method should return an object of class Class which will be converted to the internal Prolog representation of the Java object.
Prolog: [-atom]
Java: SPTerm M()
The Java method should return an object of class SPTerm which will be converted to a Prolog atom.
Prolog: [-boolean]
Java: boolean M()
The Java should return a boolean. The value will be converted to a Prolog atom in {true,false}.
Prolog: [-chars]
Java: String M()
The Java method should return an object of class String which will be converted to a list of character codes.
Prolog: [-string]
Java: String M()
The Java method should return an object of class String which will be converted to an atom.

Calling Java from Prolog: An Example

The following is an simple, but complete example of how a Java method can be called from Prolog.

First, we must write the resource file. Let us call it `simple.pl'.

% File: simple.pl

:- module(simple, [simple/2]).

:- use_module(library(jasper)).
:- load_foreign_resource(simple).

foreign(method('Simple', 'simpleMethod', [static]), java,
        simple(+integer,[-integer])).

foreign_resource(simple,
                 [
                  method('Simple', 'simpleMethod', [static])
                 ]).

This file is the processed with the script splfr (see section Interface Predicates) to produce a foreign resource:

% splfr simple simple.pl
SICStus  3.7: Mon Mar  2 19:22:44 MET 1998
{/var/tmp/aaaa004Cd.c generated, 20 msec}

yes

Note that we do not specify any Java files to splfr as we would specify C files when building foreign resources for C code. This is because the C code can be compiled into the resource itself, while the Java code must be loaded at runtime into the JVM. This means that the resource will only contain the glue-code for calling the JVM, and no actual Java code. Hence, these resources are usually quite small.

Note also that the use of +dynamic is implicit and obligatory; resources with Java-calls must be dynamic.

Now, we need some Java code to call:

Simple.java:

public class Simple
{
  static int simpleMethod(int value)
  {
    return value*42;
  }
}

This Java code must now be compiled. Refer to the documentation of your Java implementation exactly how to do this. On Solaris, this might look like:

% javac Simple.java

Now we are ready to call the method simple/2 from inside SICStus.

% sicstus
SICStus  3.7: Mon Mar  2 19:22:44 MET 1998
| ?- compile(simple).
{compiling ...}
[...]
{compiled ... simple.pl in module simple, 160 msec 48640 bytes}

yes
| ?- simple(17,X).

X = 714 ? 

yes
| ?-

What has happened is that the predicate simple/2 has been installed as a predicate defined in Java (this is not exactly true; the predicate is defined as a C-function which calls the Java method). When we load the simple module, we will first load the jasper module (and thereby the JVM) and then load the simple foreign resource, which defines the simple/2 predicate.

Calling Prolog from Java

Calling Prolog from Java is done by using the Java package jasper. This package contains a set of Java classes which can be used to create and manipulate terms, ask queries and request one or more solutions. The functionality provided by this set of classes is basically the same as the functionality provided by the C-Prolog interface (see section Mixing C and Prolog).

The usage is easiest described by an example. The following is a Java version of the train example. See section Train Example (connections).

import jasper.*;

public class Simple
{
  public static void main(String argv[]) {

  SICStus sp;
  SPPredicate pred;
  SPTerm from, to, way;
  SPQuery query;
  int i;

  try 
    {
      sp = new SICStus(argv,null);

      sp.load("train.ql");

      pred = new SPPredicate(sp, "connected", 4, "");
      to = new SPTerm(sp, "Orebro");
      from = new SPTerm(sp, "Stockholm");
      way = new SPTerm(sp).putVariable();

      query = sp.openQuery(pred, new SPTerm[] { from, to, way, way });

      while (query.nextSolution())
        {
          System.out.println(way.toString());
        }
    }
  catch ( Exception e )
    {
      e.printStackTrace();
    }
  }
}

This is how it works:

  1. First, we import all the jasper-classes. To find them, the class-path should be set to contain the path to the SICStus libraries ($SP_PATH/library). How this is done depends on the operating system and which Java implementation is used, but typically there is a environment variable CLASSPATH which contains the path. The path can often also be set on the commandline using an option such as `-classpath’. Refer to your Java documentation for more details.
  2. Before any predicates can be called, the SICStus emulator must be initialized. This is done by instantiating the SICStus class. NOTE: This class must only be instantiated once per Java process. Multiple SICStus-objects are not supported. Most methods take a reference to this object as their first argument. This is implicit in the rest of this chapter, unless otherwise stated.
  3. The next step is to load the Prolog code. This is done by the method load. Corresponds to SP_load() in the C-interface. See section Loading Prolog Code.
  4. Now, everything is set up to start making queries. In order to make a query, the actual query term must be created. This is done by creating an object of the SPPredicate class:
    SPPredicate pred = new SPPredicate(sp, "connected", 4, "");
  5. At this point, we have created a predicate object for the predicate connected/4. It is now time to create the arguments for the query. The arguments are placed in an array which is passed to a suitable method to make the query. The arguments consist of objects of the class SPTerm. For example, if we need two atoms and a variable for the query
    | ?- connected('Stockholm', 'Orebro', X, X).

    the following Java code will do it for us:

    to = new SPTerm(sp, "Orebro");
    from = new SPTerm(sp, "Stockholm");
    way = new SPTerm(sp).putVariable();
  6. Now it is time to make the query. As in the C-Prolog interface, there are three ways of making a query.
    query(pred, args)
    This method is useful if you are only interested in finding the first solution to a goal. In the case of connected/4 this is not the case; there are more than one solution.
    queryCutFail(pred, args)
    This method is useful if you are only interested in the side-effects of the query. As query() it only finds the first solution, and then it cuts away all other solutions and fails.
    openQuery(pred, args)
    This method is useful when you are interested in some or all solutions to the query. Since the connected/4 may give us multiple solution, this is what we will use.

    SPQuery query;
    
    query = sp.openQuery(pred, new SPTerm[] { from, to, way, way });
    
    while (query.nextSolution())
      System.out.println(way.toString());

    The openQuery method returns a reference to the query, an object of the SPQuery class. To obtain solutions, the method nextSolution() is called with no arguments. nextSolution returns true as long as there are more solutions, the example above will print the value of way until there are no more solutions.

Jasper Package Class Reference

Detailed documentation of the classes in the jasper package can be found at

http://www.sics.se/isl/sicstus/jasper/Package-jasper.html

Or follow this hyperlink.

This documentation will most probably be included in a future version of the manual.

Exception Handling

Exceptions are handled seamlessly between Java and Prolog. This means that exceptions can be thrown in Prolog and caught in Java and the other way around. For example, if a predicate called from Java raises an exception by raise_exception/1 and the predicate itself does not catch the exception, the Java-method which performed the query, queryCutFail() for example, will throw an exception containing the exception term. Symmetrically, a Java-exception thrown (and not caught) in a method called from Prolog will cause the corresponding predicate (simple/2 in the example above) to raise an exception containing the exception object (in the internal Prolog representation of a Java object).

Resources

There are almost infinitely many Java resources on the Internet. Here is a list of a few which are related to Jasper:

  • The SICStus Prolog Release Notes contains information useful in order to get Jasper working.
  • There is a collection of Java-to-Prolog examples in
    library('jasper/examples')

    See the file `README' for details on how to use the examples.

  • The main Java resource is Sun’s own Java homepage:
    http://java.sun.com
  • Yahoo:
    http://www.yahoo.com/Computers_and_Internet/Programming_Languages/Java/
  • Information about the JNI can be found at:
    http://java.sun.com/products/jdk/1.1/docs/guide/jni/index.html
  • The ACM student magazine Crossroads has published an article on the JNI, available online at:
    http://www1.acm.org:82/crossroads/xrds4-2/jni.html

[repost ]Running SICStus from Java

original:http://bulba.sdsu.edu/prolog/relnotes/Running-SICStus-from-Java.html

If Java is used as parent application, things are a little more complicated. There are a couple of things that need to be taken care of. The first is to specify the correct class path so that Java can find the Jasper classes (SICStusSPTerm, and so on). This is done by specifying the pathname of the file jasper.jar:

     % java -classpath $SP_PATH/bin/jasper.jar ...

SP_PATH does not need to be set; it is only used here as a placeholder. See the documentation of the Java implementation for more info on how to set classpaths.

The second is to specify where Java should find the Jasper native library (libspnative.so or spnative.dll), which the SICStus class loads into the JVM by invoking the methodSystem.loadLibrary("spnative"). Under UNIX, Jasper can usually figure this out by itself, but in the event that Jasper is used in a non-standard installation, this will most likely fail. A typical example of such a failure looks like:

     % java -classpath [...]/jasper.jar se.sics.jasper.SICStus
     Trying to load SICStus.
     Exception in thread "main" java.lang.UnsatisfiedLinkError: no spnative
     in java.library.path
     	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1133)
     	at java.lang.Runtime.loadLibrary0(Runtime.java:470)
     	at java.lang.System.loadLibrary(System.java:745)
     	at se.sics.jasper.SICStus.loadNativeCode(SICStus.java:37)
     	at se.sics.jasper.SICStus.initSICStus(SICStus.java:80)
     	at se.sics.jasper.SICStus.<init>(SICStus.java:111)
     	at se.sics.jasper.SICStus.main(SICStus.java:25)

Under UNIX, this can be fixed by explicitly setting the Java property java.library.path to the location of libspnative.so, like this:

     % java -Djava.library.path=/usr/local/lib [...]

Under Windows, Java must be able to find spnative.dll through the PATH environment variable (see Windows). Setting -Djava.library.path under Windows can lead to problems if multiple versions of SICStus has been installed.

If this works properly, SICStus should have been loaded into the JVM address space. The only thing left is to tell SICStus where the (extended) runtime library, sprt.sav (spre.sav), is located. On those platforms where the SICStus run-time system can determine its own location, e.g. Windows, Solaris and Linux, the run-time system will find the runtime library automatically. Otherwise, you may choose to specify this explicitly by either giving a second argument when initializing the SICStus object or by specifying the property sicstus.path:

Example (UNIX):

     % java -Dsicstus.path=/usr/local/lib/sicstus-3.12.3

If you do not specify any explicit path, SICStus will search for the runtime library itself.

If everything is set up correctly, you should be able to call main (which contains a short piece of test-code) in the SICStus root class, something like this:

     % java -Djava.library.path="/usr/local/lib" \
            -Dsicstus.path="/usr/local/lib/sicstus-3.12.3" \
            -classpath "/usr/local/lib/sicstus-3.12.3/bin/jasper.jar" \
             se.sics.jasper.SICStus
     Trying to load SICStus.
     If you see this message, you have successfully
     initialized the SICStus Prolog engine.

Under Windows, it would look something like this, depending on the shell used:

     % java -classpath "C:/Program Files/SICStus Prolog 3.12.3/bin/jasper.jar" se.sics.jasper.SICStus
     Trying to load SICStus.
     If you see this message, you have successfully
     initialized the SICStus Prolog engine.

If more than one se.sics.jasper.SICStus instance will be created, then the SICStus run-times named e.g. sprt312_instance_01_.dll, need to be available as well. See Runtime Systems on Target Machines (the SICStus Prolog Manual).

[repost ]Natural Language Processing Techniques in Prolog

original:http://cs.union.edu/~striegnk/courses/nlp-with-prolog/html/index.html

[repost ]Ontology Rules with Prolog

original:http://sujitpal.blogspot.com/2009/06/ontology-rules-with-prolog.html

Over the years, I’ve had an on-again, off-again interest in Rules Engines. However, as Martin Fowler points out, it is often more pragmatic to build a custom engine. A custom engine can be as simple as a properties file modelled after an awk script (ie, {pattern => action} pairs). More complex rules, ie multiple pattern matches in a certain sequence leading to a single complex action, can also be modeled by doing a Java variant of the awk strategy, ie {Predicate => Closure}. Where Rules Engines shine, however, is when you need to do rule chaining or when the structure of the rules themselves (rather than their values) change very rapidly.

Motivation

I actually set out to learn Jena Rules using the Semantic Web Programming book as a guide. Midway through that exercise, it occurred to me that Prolog would be a cleaner and almost drop-in replacement to the rather verbose Turtle syntax. Apparently the Semantic Web community thinks otherwise, since Turtle stands for Terse RDF Triple language. I haven’t actually used Prolog before this, although I’ve read code snippets in articles once or twice (but not recently), so the realization was almost like an epiphany.

Which Prolog?

I initially download GNU-Prolog because it was available from the yum repository, but then I decided to go with SWI-Prolog, because there is a Netbeans plugin available for it, and because it offers a Java-Prolog Interface (JPL) (haven’t tried this yet). Because SWI-Prolog did not have an RPM for my AMD-64 laptop, I had to build it from source, but I did not have any problems doing that.

Learning Prolog

There are quite a few Prolog tutorials available on the Web, but most focus on trying to use it as a general-purpose programming language. Since I intended to use Prolog only for its logic programming facilities, I found the Learn Prolog Now! and Adventure in Prolog online books more suitable. The first one is based on SWI-Prolog and the second on Amzi! Prolog, but examples from both worked fine for me.

The Fact Base

A Prolog program consists of facts, rules and queries. In order to keep my fact base similar to the ontology model, I decided to model my facts as triples (isTriple/3 in Prolog, since it takes three arguments), as shown below. Each of the subject, predicate and object can either be an Atom or a Compound Term (you have to make this decision at modeling time). I’ve just used Atoms in my example.

1
2
3
4
isTriple(subject, predicate, object).
% if we want predicates to have a property such as weight, we
% can model it as a compound term as shown below:
isTriple(subject, predicate(name, weight), object).

I used a simple Java program to generate my initial fact base of about 500+ triples from the sample wine.rdf file. It uses Jena to parse the file and write out the facts into a flat file. Unlike my previous usage, where I tried to map inverse relationships using an Enum, this time I only consider the relationships that exist in the wine.rdf file itself, and use rules to build the inverse relations. Since my subject and object names start with upper-case, I prepended an ‘a’ to make it conform to Prolog’s syntax rules. You can run this with a main() class or write a unit test. I used a unit test, but I am not showing this since its so trivial.

 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
// Source: src/main/java/net/sf/jtmt/inferencing/prolog/Owl2PrologFactGenerator.java
package net.sf.jtmt.inferencing.prolog;

import java.io.FileWriter;
import java.io.PrintWriter;

import org.apache.commons.lang.StringUtils;

import com.hp.hpl.jena.graph.Node;
import com.hp.hpl.jena.graph.Node_URI;
import com.hp.hpl.jena.graph.Triple;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;

/**
 * Reads an OWL file representing an ontology, and outputs a Prolog
 * fact base.
 */
public class Owl2PrologFactGenerator {

  private String inputOwlFilename;
  private String outputPrologFilename;

  public void setInputOwlFilename(String inputOwlFilename) {
    this.inputOwlFilename = inputOwlFilename;
  }

  public void setOutputPrologFilename(String outputPrologFilename) {
    this.outputPrologFilename = outputPrologFilename;
  }

  public void generate() throws Exception {
    PrintWriter prologWriter = 
      new PrintWriter(new FileWriter(outputPrologFilename), true);
    Model model = ModelFactory.createDefaultModel();
    model.read(inputOwlFilename);
    StmtIterator sit = model.listStatements();
    while (sit.hasNext()) {
      Statement st = sit.next();
      Triple triple = st.asTriple();
      String prologFact = getPrologFact(triple);
      if (StringUtils.isNotEmpty(prologFact)) {
        prologWriter.println(getPrologFact(triple));
      }
    }
    model.close();
    prologWriter.flush();
    prologWriter.close();
  }

  private String getPrologFact(Triple triple) {
    StringBuilder buf = new StringBuilder();
    Node subject = triple.getSubject();
    Node object = triple.getObject();
    if ((subject instanceof Node_URI) &&
        (object instanceof Node_URI)) {
      buf.append("isTriple(a").
      append(triple.getSubject().getLocalName()).
      append(",").
      append(triple.getPredicate().getLocalName()).
      append(",a").
      append(triple.getObject().getLocalName()).
      append(").");
    }
    return buf.toString();
  }
}

My output file contains the fact base in Prolog syntax. Here is a partial listing, to show you how it looks. The full source file (including the rules and the testing function, described below) is available here if you want it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
% Source: src/main/prolog/net/sf/jtmt/inferencing/prolog/wine_facts.pro
% ...
isTriple(aCorbansPrivateBinSauvignonBlanc,hasBody,aFull).
isTriple(aCorbansPrivateBinSauvignonBlanc,hasFlavor,aStrong).
isTriple(aCorbansPrivateBinSauvignonBlanc,hasSugar,aDry).
isTriple(aCorbansPrivateBinSauvignonBlanc,hasMaker,aCorbans).
isTriple(aCorbansPrivateBinSauvignonBlanc,locatedIn,aNewZealandRegion).
isTriple(aCorbansPrivateBinSauvignonBlanc,type,aSauvignonBlanc).
isTriple(aSevreEtMaineMuscadet,hasMaker,aSevreEtMaine).
isTriple(aSevreEtMaineMuscadet,type,aMuscadet).
isTriple(aWineFlavor,subClassOf,aWineTaste).
isTriple(aWineFlavor,type,aClass).
isTriple(aEdnaValleyRegion,locatedIn,aCaliforniaRegion).
isTriple(aEdnaValleyRegion,type,aRegion).
...

Adding Rules

The first step is adding the inverse relationships using Prolog rules. This is quite simple, as shown below:

 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
% Source: src/main/prolog/net/sf/jtmt/inferencing/prolog/wine_facts.pro
% ...
% --------------------------------------------------------------
%         rules to augment the generated facts.
% --------------------------------------------------------------

% rules to generate inverse relationships where applicable
isTriple(Subject, isVintageYearOf, Object) :-
    isTriple(Object, hasVintageYear, Subject).
isTriple(Subject, regionContains, Object) :-
    isTriple(Object, locatedIn, Subject).
isTriple(Subject, mainIngredient, Object) :-
    isTriple(Object, mainIngredient, Subject).
isTriple(Subject, isFlavorOf, Object) :-
    isTriple(Object, hasFlavor, Subject).
isTriple(Subject, isColorOf, Object) :-
    isTriple(Object, hasColor, Subject).
isTriple(Subject, isSugarContentOf, Object) :-
    isTriple(Object, hasSugar, Subject).
isTriple(Subject, isBodyOf, Object) :-
    isTriple(Object, hasBody, Subject).
isTriple(Subject, madeBy, Object) :-
    isTriple(Object, hasMaker, Subject).
isTriple(Subject, hasInstance, Object) :-
    isTriple(Object, type, Subject).
isTriple(Subject, superClassOf, Object) :-
    isTriple(Object, subClassOf, Subject).

Nothing fancy here, as you can see – we just create new isTriple rules by switching the subject and object around, and replacing the predicate with its inverse. These are simple examples of generating relationships algebrically from existing ones, we have slightly more complex examples later. Trying out some of these rules in the SWI-Prolog listener (AKA interactive shell in Python, or REPL in Lisp) shows that they work. Note that the last false indicates that there are no more matches for this rule.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
sujit@sirocco:~$ pl
Welcome to SWI-Prolog (Multi-threaded, 64 bits, Version 5.6.64)
Copyright (c) 1990-2008 University of Amsterdam.
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
Please visit http://www.swi-prolog.org for details.

?- consult('wine_facts.pro').
% wine_facts.pro compiled 0.02 sec, 109,832 bytes
true.

?- isTriple(aCongressSpringsSemillon, hasSugar, aDry).
true ;
false.

?- isTriple(aDry, isSugarContentOf, aCongressSpringsSemillon).
true ;
false.

I then decided to add relations which don’t already exist, using slightly more complex rules (involving recursion) to generate relationships from existing ones. Here is the snippet for these rules from my wine_facts.pro file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
% Source: src/main/prolog/net/sf/jtmt/inferencing/prolog/wine_facts.pro
% ...
% rule to find all wines produced by a given region (region can be at any
% level, ie. country (USRegion), state (CaliforniaRegion), or location within
% state (SantaCruzMountainsRegion). Only wines should be listed. We do this
% by ensuring that a Wine has a valid maker.
isTriple(Region, produces, Wine) :- isTriple(Region, regionContains, Wine),
                                    isTriple(Wine, hasMaker, _).
isTriple(Region, produces, Wine) :- isTriple(Region, regionContains, X),
                                    isTriple(X, produces, Wine),
                                    isTriple(Wine, hasMaker, _).

% rule to find out the region for which the wine is produced. Only the
% regions should be listed. We do this by ensuring that a Region has type
% aRegion.
isTriple(Wine, producedBy, Region) :- isTriple(Region, regionContains, Wine),
                                      isTriple(Region, type, aRegion).
isTriple(Wine, producedBy, Region) :- isTriple(Region, regionContains, X),
                                      isTriple(X, produces, Wine),
                                      isTriple(X, type, aRegion).

As before we can test these rules from the SWI-Prolog shell. However, I also built a little Prolog function that allows you to do Query-By-Example.

1
2
3
4
5
6
7
8
9
% Source: src/main/prolog/net/sf/jtmt/inferencing/prolog/wine_facts.pro
% --------------------------------------------------------------
%     simple query-by-example testing tool
% --------------------------------------------------------------
test(Subject,Predicate,Object) :- isTriple(Subject, Predicate, Object),
    tab(2), write('('), write(Subject),
    write(','), write(Predicate),
    write(','), write(Object),
    write(')'), nl, fail.

Running my test cases (commented out in the source file, since I could not get them to work in batch mode) in the SWI-Prolog listener returns the following (expected) results.

 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
?- consult('wine_facts.pro').
% wine_facts.pro compiled 0.02 sec, 109,832 bytes
true.

?- test(aUSRegion, produces, X).
  (aUSRegion,produces,aMountEdenVineyardEstatePinotNoir)
  (aUSRegion,produces,aMountEdenVineyardEdnaValleyChardonnay)
  (aUSRegion,produces,aFormanChardonnay)
  (aUSRegion,produces,aWhitehallLaneCabernetFranc)
  (aUSRegion,produces,aFormanCabernetSauvignon)
  (aUSRegion,produces,aElyseZinfandel)
  (aUSRegion,produces,aSeanThackreySiriusPetiteSyrah)
  (aUSRegion,produces,aPageMillWineryCabernetSauvignon)
  (aUSRegion,produces,aBancroftChardonnay)
  (aUSRegion,produces,aSaucelitoCanyonZinfandel)
  (aUSRegion,produces,aSaucelitoCanyonZinfandel1998)
  (aUSRegion,produces,aMariettaPetiteSyrah)
  (aUSRegion,produces,aMariettaZinfandel)
  (aUSRegion,produces,aGaryFarrellMerlot)
  (aUSRegion,produces,aPeterMccoyChardonnay)
  (aUSRegion,produces,aMariettaOldVinesRed)
  (aUSRegion,produces,aCotturiZinfandel)
  (aUSRegion,produces,aMariettaCabernetSauvignon)
  (aUSRegion,produces,aVentanaCheninBlanc)
  (aUSRegion,produces,aLaneTannerPinotNoir)
  (aUSRegion,produces,aFoxenCheninBlanc)
  (aUSRegion,produces,aSantaCruzMountainVineyardCabernetSauvignon)
  (aUSRegion,produces,aStGenevieveTexasWhite)
false.

?- test(X, produces, aLaneTannerPinotNoir).
  (aSantaBarbaraRegion,produces,aLaneTannerPinotNoir)
  (aCaliforniaRegion,produces,aLaneTannerPinotNoir)
  (aUSRegion,produces,aLaneTannerPinotNoir)
false.

?- test(aTexasRegion, produces, X).
  (aTexasRegion,produces,aStGenevieveTexasWhite)
false.

?- test(X, produces, aStGenevieveTexasWhite).
  (aCentralTexasRegion,produces,aStGenevieveTexasWhite)
  (aTexasRegion,produces,aStGenevieveTexasWhite)
  (aUSRegion,produces,aStGenevieveTexasWhite)
false.

?- test(X, producedBy, aUSRegion).
  (aCaliforniaRegion,producedBy,aUSRegion)
  (aTexasRegion,producedBy,aUSRegion)
  (aMountEdenVineyardEstatePinotNoir,producedBy,aUSRegion)
  (aMountEdenVineyardEdnaValleyChardonnay,producedBy,aUSRegion)
  (aFormanChardonnay,producedBy,aUSRegion)
  (aWhitehallLaneCabernetFranc,producedBy,aUSRegion)
  (aFormanCabernetSauvignon,producedBy,aUSRegion)
  (aElyseZinfandel,producedBy,aUSRegion)
  (aSeanThackreySiriusPetiteSyrah,producedBy,aUSRegion)
  (aPageMillWineryCabernetSauvignon,producedBy,aUSRegion)
  (aBancroftChardonnay,producedBy,aUSRegion)
  (aSaucelitoCanyonZinfandel,producedBy,aUSRegion)
  (aSaucelitoCanyonZinfandel1998,producedBy,aUSRegion)
  (aMariettaPetiteSyrah,producedBy,aUSRegion)
  (aMariettaZinfandel,producedBy,aUSRegion)
  (aGaryFarrellMerlot,producedBy,aUSRegion)
  (aPeterMccoyChardonnay,producedBy,aUSRegion)
  (aMariettaOldVinesRed,producedBy,aUSRegion)
  (aCotturiZinfandel,producedBy,aUSRegion)
  (aMariettaCabernetSauvignon,producedBy,aUSRegion)
  (aVentanaCheninBlanc,producedBy,aUSRegion)
  (aLaneTannerPinotNoir,producedBy,aUSRegion)
  (aFoxenCheninBlanc,producedBy,aUSRegion)
  (aSantaCruzMountainVineyardCabernetSauvignon,producedBy,aUSRegion)
  (aStGenevieveTexasWhite,producedBy,aUSRegion)
false.

?- test(X, producedBy, aTexasRegion).
  (aCentralTexasRegion,producedBy,aTexasRegion)
  (aStGenevieveTexasWhite,producedBy,aTexasRegion)
false.

?- test(aLaneTannerPinotNoir, producedBy, X).
  (aLaneTannerPinotNoir,producedBy,aSantaBarbaraRegion)
  (aLaneTannerPinotNoir,producedBy,aCaliforniaRegion)
  (aLaneTannerPinotNoir,producedBy,aUSRegion)
false.

?- test(aStGenevieveTexasWhite, producedBy, X).
  (aStGenevieveTexasWhite,producedBy,aCentralTexasRegion)
  (aStGenevieveTexasWhite,producedBy,aTexasRegion)
  (aStGenevieveTexasWhite,producedBy,aUSRegion)
false.

Conclusion

I found this article (PDF) describing an attempt to model OWL rules using Prolog, so perhaps this idea is not as novel as it seemed to me at first. Prolog uses backward inferencing, which means that the rule based facts are recomputed on demand, rather than at the point of being asserted into the factbase. For a query-heavy system, which most rule based systems tend to be, this can have an impact on performance. But I think an application built around Prolog’s rule engine can get around this by identifying a fact based on its origin, and generating and caching rule based facts at the point of assertion. If a rule is dropped or modified, the facts based on that rule could be recomputed and cached automatically.

In terms of simplicity of syntax alone, I think a Prolog based rule definition system would be a welcome addition to the Semantic Web Programmer’s toolkit. The pattern-based query by example I have described is also likely to be much simpler and easier to use than the more imperative SPARQL query language used to query OWL ontologies.

However, I do plan to learn how to build rules using the tools and languages in the Jena framework, just because it is what I am more likely to use in a typical Semantic Web development environment.

[repost ]Drools Petstore Example in Prolog

original:http://sujitpal.blogspot.com/2009/09/drools-petstore-example-in-prolog.html

I recently came across Prova, a Prolog dialect over the JVM using Mandarax as its inference engine. I have used SWI-Prolog before, but only as a simple inference engine for ontology relationships. Trying to develop an example in Prolog to try out with Prova, I realized that I couldn’t code Prolog to save my life, so I set about trying to remedy that. This post is a result of that effort.

The example here is based on the Drools Pet Store Example. The Petstore in our simplified example sells only (a single unspecified species of) fish, fish food and fish tanks. Fish are $5/ea, food packets are $2/ea, and fish tanks are $40/ea. At checkout, some amount of upselling and customer coddling takes place, which are enshrined in the following rules:

  • Give customer free fish food, one packet for every 5 fish (s)he bought.
  • If customer has purchased fish, ask customer how much more fish food he wants. Add this to cart.
  • If customer has purchased 10 fish or more, and no tank to put them in, ask if (s)he wants a tank. If customer says yes, add to cart.
  • For orders over $50, apply a 10% discount.

The equivalent rules in Prolog are shown below. It is very likely that the code could have been more concise, had it been written by someone with more Prolog programming experience, but c’est la vie. The idea is that checkout goal would be invoked (possibly from a Java application) at the checkout phase, and it will be passed the shopping cart as a Prolog sequence. The cart can then be queried from the factbase using cart(X). Currently the code is written as a standalone Prolog application, it will probably require some changes when it is hooked up to a Java caller.

  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
% Source: petstore.pro

% A Cart is modelled as a sequence of:
% [#-fish, #-food, #-free-food, #-tank,discount_amount].
% The following block are rules to pull out components from the
% cart and populate the required value in the output parameter.
% @param Cart (IN) - the sequence representing the shopping cart.
% @param N (OUT) - the value of the named component.
number_of_fish([NumberOfFish, _, _, _, _], N) :- 
  N is NumberOfFish.
number_of_food([_, NumberOfFood, _, _, _], N) :- 
  N is NumberOfFood.
number_of_free_food([_, _, NumberOfFreeFood, _, _], N) :- 
  N is NumberOfFreeFood.
number_of_tank([_, _, _, NumberOfTank, _], N) :- 
  N is NumberOfTank.
discount([_, _, _, _, Discount], N) :- 
  N is Discount.

% Computes the contents of the cart using the predefined prices
% @param Cart (IN) - the sequence representing the shopping cart.
% @param Total (OUT) - the computed total dollar value of the cart.
compute_cart_total(Cart, Total) :-
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_tank(Cart, NumberOfTank),
  Total is (5 * NumberOfFish) + (2 * NumberOfFood) + (40 * NumberOfTank).

% if customer has 5 or more fish, give him free fish food,
% 1 fish food packet for every 5 fish he purchases.
add_free_fish_food :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_tank(Cart, NumberOfTank),
  discount(Cart, Discount),
  NewFreeFood is floor(NumberOfFish / 5),
  append([], [NumberOfFish, NumberOfFood, NewFreeFood, 
    NumberOfTank, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).

% ask customer if he wants additional fish food.
% @param Quantity (OUT) - the quantity of additional fish food.
ask_add_fish_food(Quantity) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  ask_add_fish_food(NumberOfFish, Quantity).
ask_add_fish_food(0, Quantity) :-
  Quantity is 0.
ask_add_fish_food(NumberOfFish, Quantity) :-
  NumberOfFish > 0,
  write('>> How much more fish food to add? '), 
  read(Quantity), nl.

% add additional fish food as requested by customer, to cart.
% @param Quantity (IN) - if 0, no action. If not 0, then add to cart
% and assert it back into the factbase.
add_fish_food(0) :- !.
add_fish_food(Quantity) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  number_of_tank(Cart, NumberOfTank),
  discount(Cart, Discount),
  NewFood is NumberOfFood + Quantity,
  append([], [NumberOfFish, NewFood, NumberOfFreeFood, 
    NumberOfTank, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).

% ask customer if he wants to buy a fish tank. Only ask the question
% if the number of fish are > 10 and customer doesn't already have a
% tank in his shopping cart.
% @param Yorn (OUT) - populated as a result of this method.
ask_add_fish_tank(Yorn) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_tank(Cart, NumberOfTank),
  ask_add_fish_tank(NumberOfFish, NumberOfTank, Yorn).
ask_add_fish_tank(NumberOfFish, _, Yorn) :-
  NumberOfFish < 10, Yorn is 0, !.
ask_add_fish_tank(_, NumberOfTank, Yorn) :-
  NumberOfTank > 0, Yorn is 0, !.
ask_add_fish_tank(_, _, Yorn) :-
  write('>> Add a fish tank? [y/n] '), read(Yorn), nl.

% Adds a fish tank to the cart and asserts it into the factbase.
% This is only done if the answer to the 'add a fish tank' question
% is 'y'.
add_fish_tank('y') :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  discount(Cart, Discount),
  Quantity is 1,
  append([], [NumberOfFish, NumberOfFood, NumberOfFreeFood, 
    Quantity, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).
add_fish_tank(_) :- !.

% if order value > $50, apply 10% discount on total
apply_discount :-
  cart(Cart),
  compute_cart_total(Cart, CartTotal),
  apply_discount(CartTotal).
apply_discount(CartTotal) :-
  CartTotal < 50, !.
apply_discount(CartTotal) :-
  CartTotal >= 50,
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  number_of_tank(Cart, NumberOfTank),
  Discount is 0.1 * CartTotal,
  append([], [NumberOfFish, NumberOfFood, NumberOfFreeFood, 
    NumberOfTank, Discount], NewCart),
  retract(cart(Cart)),
  assert(cart(NewCart)).

% display contents of the cart.
% @param Heading (IN) - legend for the cart display
% @param Cart (IN) - the sequence representing the cart.
display_cart(Heading) :-
  cart(Cart),
  number_of_fish(Cart, NumberOfFish),
  number_of_food(Cart, NumberOfFood),
  number_of_free_food(Cart, NumberOfFreeFood),
  number_of_tank(Cart, NumberOfTank),
  discount(Cart, Discount),
  compute_cart_total(Cart, CartTotal),
  write('==== '), write(Heading), write(' ===='), nl,
  write('#-Fish (@ $5/ea) = '), write(NumberOfFish), nl,
  write('#-Food (@ $2/ea) = '), write(NumberOfFood),
    write(' (Free: '), write(NumberOfFreeFood), write(')'), nl,
  write('#-Tank= (@ $40/ea) = '), write(NumberOfTank), nl,
  write('- Discount Given = ($'), write(Discount), write(')'), nl,
  Total is CartTotal - Discount,
  write('** Total = $'), write(Total), write(' **'), nl.

% The top level goal that is called from the client. It will prompt
% for the cart to be entered as a sequence, and print the invoice
% after applying all the rules to the shopping cart.
checkout :-
  retractall(cart(_)),
  write('Enter cart: '), read(Cart),
  assert(cart(Cart)),
  add_free_fish_food,
  ask_add_fish_food(MoreFishFood),
  add_fish_food(MoreFishFood),
  ask_add_fish_tank(AddFishTank),
  add_fish_tank(AddFishTank),
  apply_discount,
  display_cart('Invoice'),
  !.

Here are some examples of running the code. As you can see, different rules are fired based on the contents of the shopping cart.

 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
?- consult('petstore.pro').
% petstore.pro compiled 0.00 sec, 14,304 bytes
true.

?- checkout.
Enter cart: [5,0,0,0,0].    % 5 fish in cart
>> How much more fish food to add? 2.

==== Invoice ====
#-Fish (@ $5/ea) = 5
#-Food (@ $2/ea) = 2 (Free: 1)
#-Tank= (@ $40/ea) = 0
- Discount Given = ($0)
** Total = $29 **
true.

?- cart(X).
X = [5, 2, 1, 0, 0].

?- checkout.
Enter cart: [10,0,0,0,0].   % 10 fish in cart, accept fish tank
>> How much more fish food to add? 2.

>> Add a fish tank? [y/n] y.

==== Invoice ====
#-Fish (@ $5/ea) = 10
#-Food (@ $2/ea) = 2 (Free: 2)
#-Tank= (@ $40/ea) = 1
- Discount Given = ($9.4)
** Total = $84.6 **
true.

?- cart(X).
X = [10, 2, 2, 1, 9.4].

?- checkout.
Enter cart: [10,0,0,0,0].   % 10 fish in cart, not accept fish tank
>> How much more fish food to add? 2.

>> Add a fish tank? [y/n] n.

==== Invoice ====
#-Fish (@ $5/ea) = 10
#-Food (@ $2/ea) = 2 (Free: 2)
#-Tank= (@ $40/ea) = 0
- Discount Given = ($5.4)
** Total = $48.6 **
true.

?- cart(X).
X = [10, 2, 2, 0, 5.4].

?- checkout.
Enter cart: [0,10,0,0,0].   % fish food only
==== Invoice ====
#-Fish (@ $5/ea) = 0
#-Food (@ $2/ea) = 10 (Free: 0)
#-Tank= (@ $40/ea) = 0
- Discount Given = ($0)
** Total = $20 **
true.

?- cart(X).
X = [0, 10, 0, 0, 0].

This is a somewhat contrived example, and its probably less work to actually bake these rules into the calling application as a bunch of if conditions. But it did help me to learn enough Prolog to write this… hopefully, I will be able to leverage this to write larger and more complicated rules in the future, and try this out in Prova with a Java caller.

[repost ]Calling Prolog Rules from Java

original:http://sujitpal.blogspot.com/search/label/semantic-web

Last week, I tried my hand at writing some semi-real world Prolog code by modeling the Drools Petstore Example in Prolog. Looking back now, I think the code was pretty amateurish – more expressive code could probably be written using a procedural language such as Java.

This week, I provide a somewhat improved implementation that is less procedural and more logical, or at least more in line with how I think one would code against a typical rules engine. Typically, one would call a goal on a rules engine, and the goal would either succeed or fail. If it fails, there should be some mechanism to let the client know that it failed and (optionally) what extra information it needs to proceed. The client can then call it again (and again) with the extra information, until the goal succeeds, or give up and do something else, depending on the application.

I also wanted the set of Prolog rules to be callable from Java. My main goal in learning Prolog is to be able to write rules that can be treated as configuration from a Java application – the application will retrieve and pass in the necessary facts for the Prolog rules to work on, and then use the decision/output of the rules to do whatever its supposed to. Prior to this, I have built several home grown data-driven “rule-engines” out of database tables/properties files and application code to do something similar, but I figured that Prolog would be more expressive and flexible once I learnt how to use it correctly.

Prolog Rulebase

Here is the new and improved version of the rules. As you can see, its shorter and (at least to me) cleaner. The client will call the checkout/1 goal. If the goal can proceed to completion, ie, it has all the information it needs to complete, then it will assert a cart/1 predicate into the Prolog factbase. The client will see that the goal completed successfully, and will query the factbase with cart(X). On the other hand, if there are not enough facts for the goal to complete, it will fail and assert a question/1 predicate representing the question it needs answered before it can continue. The client will see that the goal failed, and query the factbase with question(X) to get the question that needs to be answered, answer it, and call checkout/1 again with the answer appended to the original list of parameters.

 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
%% Source: petstore2.pro

:- dynamic cart/1.
:- dynamic question/1.
:- dynamic answer/2.

% A shopping cart is minimally represented as a sequence of quantities
% of [fish, food, tank]. Elements subsequent to that are answers to
% questions that the engine has asked and which it requires answers to
% in order to proceed. If provided, they are asserted into the factbase.
itemize(Cart, Fish, Food, Tank) :-
  [Fish, Food, Tank | Q] = Cart,
  extract_info(Q).
extract_info([H|R]) :-
  assert(H),
  extract_info(R).
extract_info([]) :- !.

% Add 1 packet of food for every 5 fish purchased.
add_free_food(Fish, FreeFood) :-
  FreeFood is floor(Fish / 5).

% Depending on customer's feedback, add more fish food to shopping cart.
% Only do this if the customer has bought some fish. If he is here to
% buy only food or a tank, don't even ask him about it.
add_more_food(Fish, MoreFood) :-
  Fish =< 0,
  MoreFood is 0,
  !.
add_more_food(Fish, MoreFood) :-
  Fish > 0,
  not(answer(how_many_food, _)),
  assert(question(how_many_food)),
  MoreFood is 0,
  fail.
add_more_food(Fish, MoreFood) :-
  Fish > 0,
  answer(how_many_food, MoreFood).

% If customer has bought 10 or more fish and no tank to put them in,
% ask if he wants a fish tank. If answer is already provided, add
% fish tank (or not if he says no) to cart.
add_fish_tank(Fish, _, AddTank) :-
  Fish < 10,
  AddTank is 0,
  !.
add_fish_tank(_, Tank, AddTank) :-
  Tank > 0,
  AddTank is 0,
  !.
add_fish_tank(Fish, Tank, AddTank) :-
  Fish >= 10,
  Tank == 0,
  not(answer(add_a_tank, _)),
  assert(question(add_a_tank)),
  AddTank is 0,
  fail.
add_fish_tank(Fish, Tank, AddTank) :-
  Fish >= 10,
  Tank == 0,
  answer(add_a_tank, AddTank).

% Apply a 10% discount on orders over $50.
apply_discount(Total, Discount) :-
  Total < 50,
  Discount is 0.
apply_discount(Total, Discount) :-
  Total >= 50,
  Discount is 0.1 * Total.

% The main goal which is called from the Java client. Applies the sub-goals
% in sequence, failing if there is not enough information to go ahead. If
% all information is provided, then populates a new shopping cart and asserts
% it into the factbase, from which it should be retrieved by the client.
checkout(Cart) :-
  retractall(answer(_, _)),
  retractall(question(_)),
  retractall(cart(_)),
  itemize(Cart, Fish, Food, Tank),
  add_free_food(Fish, FreeFood),
  add_more_food(Fish, MoreFood),
  TotalFood is Food + MoreFood,
  add_fish_tank(Fish, Tank, AddTank),
  Total is (5 * Fish) + (2 * TotalFood) + (40 * AddTank),
  apply_discount(Total, Discount),
  append([], [Fish, TotalFood, AddTank, FreeFood, Discount], NewCart),
  assert(cart(NewCart)),
  !.

A typical session within the SWI-Prolog listener application looks like this. This is the same flow that the Java client will use as well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
?- consult('petstore2.pro').
% petstore2.pro compiled 0.00 sec, 0 bytes
true.
?- checkout([10, 1, 0]).
false.
?- question(X).
X = how_many_food.
?- checkout([10, 1, 0, answer(how_many_food,2)]).
false.
?- question(X).
X = add_a_tank.
?- checkout([10, 1, 0, answer(how_many_food,2), answer(add_a_tank,1)]).
true.
?- cart(X).
X = [10, 3, 1, 2, 5.6].

Java Connectivity: Setting up JPL

For calling the rulebase from within Java, I used JPL, a native (JNI) Java library which provides bi-directional connectivity between SWI-Prolog and Java, and which comes bundled with the version of my SWI-Prolog (5.6.64). I copied the JAR file from the distribution into my Maven repository and added it as a dependency to my POM. Here is the snippet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<project...>
  <dependencies>
    ...
    <dependency>
      <groupId>swiprolog</groupId>
      <artifactId>jpl</artifactId>
      <version>5.6.64</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
  ...
</project>

At this point, I could write and compile the client but not run it, since it kept failing with the error “UnsatisfiedLinkError: no jpl in java.library.path”. Googling for solutions, I came across many pages like this one, but none of them worked for me – perhaps they are all Windows centric?. Ultimately, I remembered using LD_LIBRARY_PATH back in the days when I used JNI libraries, and setting that in my environment (I use Linux, Fedora Core 9, in case that helps) worked instantly with no other change. Here is the snippet from my .bash_profile. Your SWI_Prolog installation directory may be different if you are installing from a package, I had to compile mine from source because there were no distributions on 64-bit platforms.

1
2
# Prolog/JPL native access
export LD_LIBRARY_PATH=/usr/local/lib/pl-5.6.64/lib/x86_64-linux

Java Client

The JPL library has a very clean, minimalistic feel. The code here is based on the examples in the JPL Getting Started page. One caveat – the documentation is either outdated or incorrect, at least in the “Querying with Variables” section – I wasted quite a bit of time trying to figure out why I could not get back the value of the variable from the Prolog rulebase. Ultimately it turned out that I was not naming the variable, and looking up with the Variable reference as shown in the page. The code here pointed me in the right direction. So anyway, here is the Java code for the client.

A “real” client would instantiate the client, and call its init() method, then call the checkout(PetstoreCart) method with various values of PetstoreCart, and finally terminate it with the destroy() method. The checkout(PetstoreCart) method calls the _checkout(PetstoreCart) method in a loop until the cart is fully processed, then returns it.

It is worth pointing out that the _checkout(PetstoreCart) is synchronized. This is because there is only a single Prolog engine per JVM, so access from multiple threads must be serialized. When client code calls _checkout, it passes in the input cart data (see PetstoreCart.clone()), along with an answer map of questions already answered. The first 3 lines of the Prolog checkout/1 goal cleans up any data left over from a previous call. Within the synchronized _checkout(PetstoreCart) code, the code first makes a call to checkout/1, then based on the goal status, makes another call to either cart/1 or question/1.

  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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// Source: src/main/java/net/sf/jtmt/inferencing/prolog/PrologPetstoreClient.java
package net.sf.jtmt.inferencing.prolog;

import java.io.Console;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import jpl.Atom;
import jpl.JPL;
import jpl.Query;
import jpl.Term;
import jpl.Util;
import jpl.Variable;
import jpl.fli.Prolog;

/**
 * A JPL client to the Prolog rules for the Drools Petstore Example.
 * The basic idea is that the client calls the checkout goal repeatedly
 * until it succeeds. If it fails, the checkout goal will assert a 
 * question which needs to be answered before the goal can proceed
 * further, which the Java client reads and answers.
 */
public class PrologPetstoreClient {

  private final String RULES_FILE = 
    "src/main/prolog/net/sf/jtmt/inferencing/prolog/petstore2.pro";

  /**
   * Connects to the engine and injects the rules file into it. This is
   * done once during the lifetime of the engine.
   * @throws Exception if thrown.
   */
  public void init() throws Exception {
    JPL.init();
    Query consultQuery = new Query("consult", new Term[] {
      new Atom(RULES_FILE)});
    if (! consultQuery.hasSolution()) {
      throw new Exception("File not found: " + RULES_FILE);
    }
    consultQuery.close();
  }

  /**
   * Stops the prolog engine.
   * @throws Exception if thrown.
   */
  public void destroy() throws Exception {
    Query haltQuery = new Query("halt");
    haltQuery.hasSolution();
    haltQuery.close();
  }

  /**
   * Called by client after the PetstoreCart reaches the checkout phase.
   * This method is unsynchronized because it will go back and forth for
   * user input. This is the version that is expected to be called from
   * the client.
   * @param cart the Petstore cart object.
   * @return the processed cart object.
   */
  public PetstoreCart checkout(PetstoreCart cart) {
    return checkout(cart, null);
  }

  /**
   * Overloaded version for testing. This contains a hook to supply the
   * answers to the questions, so we don't have to enter them at the 
   * command prompt.  
   * @param cart the PetstoreCart object to checkout.
   * @param answers a Map of question and answer.
   * @return the processed PetstoreCart object.
   */
  protected PetstoreCart checkout(PetstoreCart cart,
      Map<String,String> answers) {
    PetstoreCart newCart = null;
    for (;;) {
      newCart = _checkout(cart);
      if (newCart.isInProgress()) {
        String question = newCart.getQuestion();
        String answer = prompt(question, answers);
        newCart.getAnswers().put(question, answer);
      } else {
        break;
      }
    }
    return newCart;
  }

  /**
   * Prompt the user for the question and waits for an answer, or
   * gets the answer from the answers map, if provided.
   * @param question the question to answer.
   * @param answers a Map of question and answer.
   * @return the answer to the question.
   */
  private String prompt(String question, Map<String,String> answers) {
    if (answers == null) {
      Console console = System.console();
      if (console != null) {
        return console.readLine(">> " + question + "?");  
      } else {
        throw new RuntimeException("No console, start client on OS prompt");
      }
    } else {
      // run using a map of predefined answers (for testing).
      if (answers.containsKey(question)) {
        String answer = answers.get(question);
        System.out.println(">> " + question + "? " + answer + ".");
        return answer;
      } else {
        throw new RuntimeException("No answer defined for question:[" + 
          question + "]");
      }
    }
  }

  /**
   * Called from checkout. This is the method that actually issues the
   * checkout query to the Prolog engine, and in case of failure, retrieves
   * the question to be answered, and in case of success, retrieves back
   * the Cart object from the Prolog engine. 
   * @param cart the current form of the PetstoreCart object.
   * @return the processed PetstoreCart object.
   */
  private synchronized PetstoreCart _checkout(PetstoreCart cart) {
    Query checkoutQuery = new Query("checkout", 
      new Term[] {buildPrologCart(cart)});
    boolean checkoutSuccessful = checkoutQuery.hasSolution();
    checkoutQuery.close();
    if (checkoutSuccessful) {
      // succeeded, get cart from factbase
      Variable X = new Variable("X");
      Query cartQuery = new Query("cart", new Term[] {X});
      Term prologCart = (Term) cartQuery.oneSolution().get("X");
      PetstoreCart newCart = parsePrologCart(prologCart);
      newCart.setInProgress(false);
      cartQuery.close();
      return newCart;
    } else {
      // failed, get question, and stick it into question
      PetstoreCart newCart = cart.clone();
      Variable X = new Variable("X");
      Query questionQuery = new Query("question", new Term[] {X});
      newCart.setQuestion(
        String.valueOf(questionQuery.oneSolution().get("X")));
      newCart.setInProgress(true);
      questionQuery.close();
      return newCart;
    }
  }

  /**
   * Builds a Term representing a Prolog List object from the contents 
   * of a PetstoreCart. The List is passed to the checkout goal. 
   * @param cart the PetstoreCart object.
   * @return a Term to pass to the checkout goal.
   */
  private Term buildPrologCart(PetstoreCart cart) {
    StringBuilder prologCart = new StringBuilder();
    prologCart.append("[").
      append(String.valueOf(cart.getNumFish())).
      append(",").
      append(String.valueOf(cart.getNumFood())).
      append(",").
      append(String.valueOf(cart.getNumTank()));
    Map<String,String> answers = cart.getAnswers();
    for (String question : answers.keySet()) {
      prologCart.append(",").
        append("answer(").
        append(question).append(",").
        append(answers.get(question)).
        append(")");
    }
    prologCart.append("]");
    return Util.textToTerm(prologCart.toString());
  }

  /**
   * Parses the returned Term object representing a Prolog List that
   * represents the processed contents of the cart. The Term is 
   * returned as a compound term of nested "." (concat) functions.
   * @param prologCart the Term representing the Cart, returned from
   *        querying Prolog with cart(X).
   * @return a PetstoreCart object.
   */
  private PetstoreCart parsePrologCart(Term prologCart) {
    List<String> elements = new ArrayList<String>();
    Term term = prologCart;
    for (;;) {
      if (term.type() == Prolog.COMPOUND) {
        elements.add(term.arg(1).toString());
        term = term.arg(2);
      } else if (term.type() == Prolog.ATOM) {
        break;
      }
    }
    PetstoreCart cart = new PetstoreCart(
      Integer.valueOf(elements.get(0)),
      Integer.valueOf(elements.get(1)), 
      Integer.valueOf(elements.get(2)));
    cart.setNumFreeFood(Integer.valueOf(elements.get(3)));
    cart.setDiscount(Float.valueOf(elements.get(4)));
    return cart;
  }
}

The PetstoreCart class is a simple bean with a custom clone() and prettyPrint() methods. The bean is shown (without the getter/setter methods for brevity) below:

 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
// Source: src/main/java/net/sf/jtmt/inferencing/prolog/PetstoreCart.java
package net.sf.jtmt.inferencing.prolog;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

/**
 * Simple bean to hold shopping cart information for PrologPetstoreClient.
 */
public class PetstoreCart {

  private int numFish;
  private int numFood;
  private int numTank;

  private String question;
  private Map<String,String> answers = 
    new HashMap<String,String>();
  private int numFreeFood;
  private float discount;
  private boolean inProgress;

  public PetstoreCart(int numFish, int numFood, int numTank) {
    this.numFish = numFish;
    this.numFood = numFood;
    this.numTank = numTank;
  }

  @Override
  public PetstoreCart clone() {
    PetstoreCart clone = new PetstoreCart(numFish, numFood, numTank);
    clone.setAnswers(getAnswers());
    return clone;
  }

  public String prettyPrint() {
    StringBuilder buf = new StringBuilder();
    buf.append("[").append(String.valueOf(getNumFish())).
      append(", ").append(String.valueOf(getNumFood())).
      append(", ").append(String.valueOf(getNumTank())).
      append(", ").append(String.valueOf(getNumFreeFood())).
      append(", ").append(String.valueOf(getDiscount())).
      append("]");
    return buf.toString();
  }
  ...
}

The corresponding unit test for the client is shown below. I test several common boundary cases to make sure that the code works.

 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
// Source: src/test/java/net/sf/jtmt/inferencing/prolog/PrologPetstoreClientTest.java
package net.sf.jtmt.inferencing.prolog;

import java.util.Map;

import org.apache.commons.lang.ArrayUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Test cases to exercise the Prolog Petstore JPL client.
 */
public class PrologPetstoreClientTest {

  private static PrologPetstoreClient CLIENT;

  @BeforeClass
  public static void setupBeforeClass() throws Exception {
    CLIENT = new PrologPetstoreClient();
    CLIENT.init();
  }

  @AfterClass
  public static void teardownAfterClass() throws Exception {
    CLIENT.destroy();
  }

  @Test
  public void test5_0_0() throws Exception {
    Map<String,String> answers = ArrayUtils.toMap(new String[][] {
      new String[] {"how_many_food", "0"},
      new String[] {"add_a_tank", "0"}
    });
    System.out.println("?- checkout([5, 0, 0]).");
    PetstoreCart cart = CLIENT.checkout(new PetstoreCart(5, 0, 0), answers);
    System.out.println("?- cart(" + cart.prettyPrint() + ").");
    System.out.println();
  }

  @Test
  public void test10_0_0() throws Exception {
    Map<String,String> answers = ArrayUtils.toMap(new String[][] {
      new String[] {"how_many_food", "5"},
      new String[] {"add_a_tank", "0"}
    });
    System.out.println("?- checkout([10, 0, 0]).");
    PetstoreCart cart = CLIENT.checkout(new PetstoreCart(10, 0, 0), answers);
    System.out.println("?- cart(" + cart.prettyPrint() + ").");
    System.out.println();
  }

  @Test
  public void test10_0_0_1() throws Exception {
    Map<String,String> answers = ArrayUtils.toMap(new String[][] {
      new String[] {"how_many_food", "5"},
      new String[] {"add_a_tank", "1"}
    });
    System.out.println("?- checkout([10, 0, 0]).");
    PetstoreCart cart = CLIENT.checkout(new PetstoreCart(10, 0, 0), answers);
    System.out.println("?- cart(" + cart.prettyPrint() + ").");
    System.out.println();
  }

  @Test
  public void test0_10_0() throws Exception {
    Map<String,String> answers = ArrayUtils.toMap(new String[][] {
      new String[] {"how_many_food", "0"},
      new String[] {"add_a_tank", "0"}
    });
    System.out.println("?- checkout([0, 10, 0]).");
    PetstoreCart cart = CLIENT.checkout(new PetstoreCart(0, 10, 0), answers);
    System.out.println("?- cart(" + cart.prettyPrint() + ").");
    System.out.println();
  }
}

Here is the output from the JUnit test run. For completeness, one should put in Assert calls, but as you can see, the cart(X) values look correct. Another thing to notice is that based on the inputs, not all the questions are required everytime, so the system asks them only as needed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
?- checkout([5, 0, 0]).
>> how_many_food? 0.
?- cart([5, 0, 0, 1, 0.0]).

?- checkout([10, 0, 0]).
>> how_many_food? 5.
>> add_a_tank? 0.
?- cart([10, 5, 0, 2, 6.0]).

?- checkout([10, 0, 0]).
>> how_many_food? 5.
>> add_a_tank? 1.
?- cart([10, 5, 1, 2, 10.0]).

?- checkout([0, 10, 0]).
?- cart([0, 10, 0, 0, 0.0]).

What about Prova?

I did take another look at Prova, this time in anger, because I struggled quite a bit to get the JNI stuff to work. However, it turns out that Prova’s Prolog dialect is different from SWI-Prolog, because it failed to parse my rulebase. I did not want to learn Prova and rewrite the Prolog code in Prova at that point, so I (fortunately, it turns out) decided to take another crack at JPL.

Another thing about Prova is that it comes with a lot of dependencies. That should probably not be a concern in our brave new Mavenized world, as long as Prova exposes the dependencies in its meta information. I don’t know for sure either way, although they probably do since their JAR file appears to be built by Maven based on its naming convention. But in any case, I decided to stick with SWI-Prolog and JPL for now – just too lazy to learn another language right now, I guess.

One other thing that attracted me to Prova in the first place is its ability to connect to a database from within the rulebase and potentially generate facts directly from it. However, this is also possible to do with JPL (indirectly, by asserting facts into the rulebase from the Java client) as this post describes.