Introduction
JPC is a Java-Prolog interoperability library based on the notion of inter-language conversions. It provides different layers of integration abstractions. This guide presents the different JPC features supporting the integration of Java-Prolog systems, assuming a general understanding of the JPC architecture.
You will notice that JPC often provides more than one mechanism to accomplish certain tasks. Typically, a simple straightforward way that is good enough for many small problems, and more sophisticated alternatives that may be more appropriate for complex requirements. If you are reading this guide for first time, it may be a good idea to start with the simpler alternatives (always presented first) and leave the more complex options for later.
Several examples in this section refer to the London underground case study. In order to download and try these examples, please refer to the JPC installation guide.
Creating Prolog Engines
JPC provides an abstraction of a Prolog engine that can interoperate with concrete Prolog engines by means of drivers. This section describes how to instantiate such engines.
Explicit Instantiation
The easiest way to instantiate a Prolog engine is by explicitly referencing a JPC driver. For example, to instantiate a JPC engine using the PDT-based driver we could write:
PrologEngine engine = new PdtConnectorDriver().createPrologEngine();
This approach, although straightforward and good enough for many scenarios, causes a strong coupling between an application and a concrete driver. An alternative is presented below.
Categorized Prolog Engines
Several techniques and conventions presented in this section are inspired on Apache log4j, a well-known logging library.
JPC allows to easily categorize an engine space
(i.e., the space of all possible Prolog engines) according to some developer-chosen criteria.
This approach allows to decouple a Java program from concrete engine implementations and provides fine grained control on the Prolog engines that should be used to accomplish a given task.
Using this technique, engines are categorized hierarchically following a naming rule.
In this hierarchy, a name "org.jpc"
is said to be the ancestor of the name "org.jpc.Jpc"
.
An engine categorized by means of a name will be inherited, unless overridden, by any of its name descendants.
This categorization is illustrated by the figure below.
An engine space can be defined either programmatically or by means of a settings file. The latter is the JPC recommended approach.
A JPC configuration can be represented (e.g., in a settings file) as a JSON object.
One of the attributes of this configuration object is engines
, a list of engine configurations.
The following JPC configuration specifies a Logtalk compatible Prolog engine categorized under the name "org.jpc"
.
{ "engines": [ { "id": "pdt", "categoryNames": ["org.jpc"], "factoryClass": "org.jpc.engine.pdtconnector.PdtConnectorDriver", "profile": "org.jpc.engine.profile.LogtalkEngineProfile" } ] }
Each engine configuration has the following attributes:
id
: The identifier of this engine configuration. This attribute is optional if thecategoryNames
attribute is specified.categoryNames
: A list of category names to which this engine configuration applies. This attribute is optional if theid
attribute is specified.factoryClass
: A factory of Prolog engines (usually a JPC driver) associated to this engine configuration.factoryMethod
: A (static or instance) method in the factory class that creates a Prolog engine. This attribute is optional. If no specified, the factory class should implement thePrologEngineFactory
interface.profile
: A decorator that may configure the Prolog engine the first time it is instantiated. It must be a class implementing thePrologEngineProfile
interface. This attribute is optional.
Although Prolog engines can be categorized by means of any name, the JPC convention is to categorize them according to the fully qualified name of the class or package where they are used.
Therefore, an engine categorized for the org.jpc
package will be used in the org.jpc.Jpc
class unless overridden (e.g., if another engine has been categorized for the name "org.jpc.Jpc"
).
An attempt to categorize a Prolog engine under an already used category name will result in an exception.
To obtain a Prolog engine in an arbitrary class, you need to write:
import static org.jpc.engine.prolog.PrologEngines.getPrologEngine; ... getPrologEngine(getClass().getCanonicalName()); //returns a Prolog engine categorized for this class fully qualified name. //or just: getPrologEngine(getClass());
Alternatively, a Prolog engine may be instantiated by means of the id
of an engine configuration, like:
import static org.jpc.engine.prolog.PrologEngines.getPrologEngineById; ... getPrologEngineById("pdt");
The engines obtained in the previous examples are instantiated from the default settings file jpc.settings
in the root package.
Alternatively, engines can be instantiated from an arbitrary settings file as in this example:
import org.jpc.util.config.EngineConfigurationManager; ... EngineConfigurationManager.createFromFile(<settings_file_name>).getPrologEngine(getClass());
The Low-Level Querying API
This section introduces the JPC low-level API for creating and manipulating Prolog queries. This API, inspired by the JPL library, provides methods that do not perform automatic conversions between Prolog and Java artifacts. Instead, a query goal and its results are always expressed by means of Prolog terms. An alternative to this approach will be presented later.
In JPC, a Prolog query is reified as a Java class named Query.
This class provides methods for executing
deterministic,
semi-deterministic and
non-deterministic queries.
A JPC query has three possible states:
READY
, OPEN
and EXHAUSTED
. Figure 2 illustrates the transition between these states by means of a state diagram chart for a JPC query. As shown in the figure, independently of its current state, the close() method always takes it to the READY
state. Any attempt to invoke a method that is illegal in the current state raises an IllegalStateException
exception.
A query solution is represented as a map binding logic variables to Prolog terms. Keys of this map are strings representing variable names. Values are terms bound to these logic variables.
An an example of the low-level querying API, consider the London underground case study.
A solution to this problem requires us to provide an implementation for the interfaces
Metro
,
Line
and
Station
.
Since methods in those interfaces frequently need to convert between instances of our Java domain classes and their Prolog term representation, we implement conversion routines that perform such operations and locate them in the corresponding implementation classes.
The code listing below shows an extract of the implementation of the Station
interface providing such conversion routines.
The asTerm()
instance method (lines 17-19) returns the representation, as a Prolog term, of the receiver Station
instance.
The create
static method (lines 5-7) returns a new Station
instance from its Prolog term representation.
Equivalent methods are also implemented in other classes in our domain not showed here.
public class StationLLApi implements Station { public static final String STATION_FUNCTOR_NAME = "station"; public static StationLLApi create(Term stationTerm) { return new StationLLApi(((Atom)stationTerm.arg(1)).getName()); } private final String name; private final PrologEngine prologEngine; public StationLLApi(String name) { this.name = name; prologEngine = getPrologEngine(getClass()); } public Compound asTerm() { return new Compound(STATION_FUNCTOR_NAME, asList(new Atom(getName()))); } @Override public Station connected(Line line) { String stationVarName = "Station"; Term message = new Compound("connected", asList(new Var(stationVarName), ((LineLLApi)line).asTerm())); Term goal = new Compound(LogtalkConstants.LOGTALK_OPERATOR, asList(asTerm(), message)); Query query = prologEngine.query(goal); Term stationTerm = query.oneSolutionOrThrow().get(stationVarName); return create(stationTerm); } ... // other methods declared in the Station interface. }
The code also shows the implementation of the connected(Line)
method
declared in the Station
interface.
Line 24 defines a Logtalk message with the form connected(Station, a_line)
, where Station
is an unbound logic variable and a_line
the term representation of the line received as a parameter.
A goal is created in the next line with the form ::(this_station, message
), where this_station
is the term representation of the Station
instance receiving the method call and message
the Logtalk message created on line 24.
A query is instantiated in line 26 from this goal.
The binding of the Station
Prolog variable corresponding to the first solution is found on line 27, and a new Station
instance is created and returned from this term representation on line 28.
Next section will set the basis for writing, in a more convenient way, the common conversion operations reviewed in this section.
The Conversion API
JPC provides an API, inspired by Google's Gson, for converting between Java Objects into their Prolog term representation and back. This conversion API can work with any kind of objects, including those whose source code is not available. It provides constructs for:
- Defining (bidirectional) Java-Prolog conversion functions (converters).
- Inferring the best target type of a conversion operation (type solvers).
- (Optionally) instantiating conversion target types (factories).
The JPC Context
The primary JPC class is a conversion context, modelled by the Jpc
class.
This context encapsulates a bidirectional conversion strategy for a set of Java objects and Prolog terms.
A context is configured in term of converters, type solvers, factories and other conversion artefacts.
Figure 3 illustrates its main components and a typical interaction with a user of the library.
In order to facilitate the configuration of a Jpc
instance, there is also a JpcBuilder
class with a straightforward fluent API for configuring its properties. This configuration involves the registration of custom conversion artefacts (e.g., converters, type solvers and factories).
The example below shows how to configure a builder to create a custom Jpc
context that knows how to convert objects from the London underground case study
.
public static final Jpc jpc = JpcBuilder.create() .register(new MetroConverter()) .register(new LineConverter()) .register(new StationConverter()).build();
Examples of conversions employing this custom JPC context are shown below:
Term term = new Compound("station", asList(new Atom("central-park"))); //a Prolog term representing the station with name "central-park" Station station = jpc.fromTerm(term); //a Java object representing the station with name "central-park" assertEquals(term, jpc.toTerm(station));
In addition to allowing custom conversions, JPC is bundled with a predefined catalog of converters that support a considerable number of common conversions. This minimises the amount of code to be written when defining new conversions.
Implementing Converters
JPC provides two interfaces for defining conversions from Java to Prolog (ToTermConverter
) and vice versa (FromTermConverter
).
The StationConverter
class in the code below implements both interfaces. It implements a method defining the conversion of Station
instances to (lines 7-9) and from (lines 11-14) a Prolog compound term.
public class StationConverter implements ToTermConverter<Station, Compound>, FromTermConverter<Compound, Station> { public static final String STATION_FUNCTOR = "station"; @Override public Compound toTerm(Station station, Class<Compound> termClass, Jpc context) { return new Compound(STATION_FUNCTOR, asList(new Atom(station.getName()))); } @Override public Station fromTerm(Compound term, Type type, Jpc context) { String stationName = ((Atom)term.arg(1)).getName(); return new Station(stationName); } }
If a converter is unable to perform a conversion operation, it should signal it by means of throwing a ConversionException
exception.
Custom converters can be registered in a JPC context by means of the JpcBuilder.register(JpcConverter)
method (and its overloaded variations) in the JpcBuilder
class.
Implementing Type Solvers
When no type information is provided in a conversion, JPC attempts to infer the best target type based on the actual source object to convert.
For example, a Prolog list term with a certain structure may be reified, by convention, as a map in Java.
Type solvers provide a mechanism for telling JPC what is the best target type in a conversion operation.
The code below shows an extract of a type solver.
Note that it implements the TypeSolver
interface.
In this example, the type solver returns a Map
class on line 13 if it can conclude that the term to convert looks like a map in Java.
If it is unable to assign a conversion type to the term it signals it throwing a UnrecognizedObjectException
exception (line 15).
public class MapTypeSolver implements TypeSolver<Compound> { @Override public Type getType(Compound term) { if(term.isList()) { ListTerm list = term.asList(); Predicate<Term> isMapEntry = new Predicate<Term>() { @Override public boolean apply(Term term) { return isMapEntry(term); } }; if(!list.isEmpty() && Iterables.all(list, isMapEntry)) return Map.class; } throw new UnrecognizedObjectException(); } private boolean isMapEntry(Term term) { ... } }
Custom type solvers can be registered in a JPC context by means of the JpcBuilder.register(TypeSolver)
method (and its overloaded variations) in the JpcBuilder
class.
Implementing Factories
If a converter does not know how to instantiate a conversion target type (e.g., it is abstract), it can ask the Jpc
context for an instance of such type.
For instance, in the type solver example above a type solver may identify the type of a Prolog list with a certain structure as a Map
. But the type solver does not provide any mechanism to instantiate such a type, since its only responsibility is to give a hint on the appropriate conversion type.
Assuming that a registered factory can instantiate Java maps, a converter only needs to invoke the instantiate(Type)
method in a Jpc
context to obtain an instance of the desired type.
An example of this simple factory is shown below.
Note that it implements the Factory
interface.
public class MapFactory implements Factory<Map<?,?>>() { @Override public Map<?,?> instantiate(Type type) { return new HashMap<>(); } }
If a factory is unable to instantiate a type, it should signal it by means of throwing a FactoryException
exception.
Custom factories can be registered in a JPC context by means of the JpcBuilder.register(Factory)
method in the JpcBuilder
class.
Primitives Conversions
In this section we illustrate how to convert between Java and Prolog primitives. In order to facilitate the discussion, we also include in this section conversions of the Java String
class, since it is the natural equivalent of the Atom
primitive Prolog type.
The simplest way to use our library is by means of the toTerm(Term)
and fromTerm(Term)
methods in the Jpc
class introduced before.
Below there is a list of successful assertions that illustrates some pre-defined conversions of Java types to Prolog terms.
assertEquals(new Atom("true"), jpc.toTerm(true)); //Boolean to Atom assertEquals(new Atom("c"), jpc.toTerm('c')); //Character to Atom assertEquals(new Atom("1"), jpc.toTerm("1")); //String to Atom assertEquals(new IntegerTerm(1), jpc.toTerm(1)); //Integer to IntegerTerm assertEquals(new FloatTerm(1), jpc.toTerm(1D)); //Double to FloatTerm
Pre-defined conversions of Prolog terms to Java types are shown below.
assertEquals(true, jpc.fromTerm(new Atom("true"))); //Atom to Boolean assertEquals("c", jpc.fromTerm(new Atom("c"))); //Atom to String assertEquals("1", jpc.fromTerm(new Atom("1"))); //Atom to String assertEquals(1L, jpc.fromTerm(new IntegerTerm(1))); //IntegerTerm to Long assertEquals(1D, jpc.fromTerm(new FloatTerm(1))); //FloatTerm to Double
Typed Conversions
The Jpc
class conversion methods can receive as a second parameter the expected type of the converted object.
Some examples of Java-Prolog conversions that specify the expected Prolog term type are shown below.
In line 1, the Integer
1 is converted to an Atom
instead of an IntegerTerm
. This is because we specify the Atom
class as the target conversion type.
In line 2, the string "1"
is converted to an IntegerTerm
.
assertEquals(new Atom("1"), jpc.toTerm(1, Atom.class)); //Integer to Atom assertEquals(new IntegerTerm(1), jpc.toTerm("1", IntegerTerm.class)); //String to IntegerTerm
Below there are examples of Prolog-Java conversions that specify the expected Java type.
assertEquals(1, jpc.fromTerm(new Atom("1"), Integer.class)); //Atom to Integer assertEquals("1", jpc.fromTerm(new IntegerTerm(1), String.class)); //IntegerTerm to String assertEquals("true", jpc.fromTerm(new Atom("true"), String.class)); //Atom to String assertEquals('c', jpc.fromTerm(new Atom("c"), Character.class)); //Atom to Character
Multi-Valued Conversions
The default Jpc catalog of converters also provides conversions for multi-valued data types such as arrays, collections, and maps. Below there is an example showing a conversion of an array object with a string and an integer element: {"apple", 10}
.
Its result is a Prolog term list having as elements an atom and an integer term: [apple, 10]
.
Alternatively, we could have used a list instead of an array. For example, we would have obtained exactly the same result by replacing line 1 by:
Term term = jpc.toTerm(asList("apple", 10));
Term term = jpc.toTerm(new Object[]{"apple", 10}); assertEquals( new Compound(".", asList(new Atom("apple"), // equivalent to .(apple, .(10, [])) new Compound(".", asList(new IntegerTerm(10), new Atom("[]"))))), term);
A slightly more complex example is illustrated below. First, a Java map is instantiated (lines 1-4).
The default term conversion is applied on line 5, generating a Prolog list with two key-value pairs: [apple:10, orange:20]
. This result is tested on lines 7-8.
Map<String, Integer> map = new LinkedHashMap<String, Integer>() {{ // LinkedHashMap preserves insertion order put("apple", 10); put("orange", 20); }}; Term term = jpc.toTerm(map); List<Term> listTerm = term.asList(); // converts a Prolog list term to a list of terms assertEquals(new Compound(":", asList(new Atom("apple"), new IntegerTerm(10))), listTerm.get(0)); assertEquals(new Compound(":", asList(new Atom("orange"), new IntegerTerm(20))), listTerm.get(1));
Generic Types Support
JPC provides extensive support for generic types.
Consider the example below.
A Prolog list term is created on line 1. We use the TypeToken
class (from Google's Guava library
) to obtain an instance of the parameterised type List<String>
(line 2).
Then we give this type as a hint to the converter (line 3) and we verify on lines 4 and 5 that the elements of the Java List
are indeed instances of String
, as it was specified on line 3.
Term listTerm = listTerm(new Atom("1"), new Atom("2")); // creates a list term from a list of terms Type type = new TypeToken<List<String>>(){}.getType(); List<String> list = jpc.fromTerm(listTerm, type); assertEquals("1", list.get(0)); assertEquals("2", list.get(1));
In the previous example, the type passed to the converter was redundant, since elements in the Prolog list are atoms, which are converted by default to String
instances in Java.
Consider, however, the example below. The main change w.r.t. the previous example is that the type we send as a hint is now List<Integer>
(line 3).
This instructs the converter to instantiate a list where all its elements are integers, as demonstrated on lines 4 and 5.
Term listTerm = listTerm(new Atom("1"), new Atom("2")); Type type = new TypeToken<List<Integer>>(){}.getType(); List<Integer> list = jpc.fromTerm(listTerm, type); assertEquals(1, list.get(0)); assertEquals(2, list.get(1));
Inference of Conversion Target Types
A previous example
showed the implementation of a type solver responsible of determining if the best conversion type of a Prolog term is a Java Map
.
Below we show a conversion example that relies on such type solver.
Note that this type solver is part of the default JPC type solvers catalog, so it does not have to be explicitly registered in a default conversion context.
On line 3 we create a list term from two previously created compound terms.
We convert it to a Java map on line 4 and test its values on lines 5 and 6.
As expected, our library infers that the best Java type of the term should be a Map
.
This is because the type solver finds that all the elements in the Prolog list ([apple:10, orange:20]
) are compounds with an arity of 2 and with functor ':'
, which are mapped by default to map entries (i.e., instances of the Map.Entry
class).
Compound c1 = new Compound(":", asList(new Atom("apple"), new IntegerTerm(10))); Compound c2 = new Compound(":", asList(new Atom("orange"), new IntegerTerm(20))); Term listTerm = listTerm(c1, c2); Map map = jpc.fromTerm(listTerm); assertEquals(10L, map.get("apple")); assertEquals(20L, map.get("orange"));
Alternatively, line 4 could be replaced by List list = jpc.fromTerm(listTerm, List.class);
The type hint List.class
given by the programmer has higher priority that the one inferred by the type solver.
In this case, the result would therefore be a list of map entries since the Prolog list would be converted to a Java list (i.e., an instance of a class implementing List
), but the default conversion of each term in the list (a compound with arity 2 and functor ':'
) would still be a map entry object.
Note that JPC leaves to the programmer the responsibility of providing enough information (i.e., a target type) in case where ambiguities are possible.
Term-Quantified Converters
A JPC context can associate converters to arbitrary Prolog terms.
This provides a convenient mechanism for limiting the application scope of converters.
This is better explained with an example.
Consider the following HelloConverter
that knows how to converter from compounds to the string "<Greeting> <Name>"; where <Greeting> is the name of the compound and <Name> its first argument.
class HelloConverter implements FromTermConverter<Compound, String> { @Override public String fromTerm(Compound term, Type targetType, Jpc context) { return ((Atom)term.getName()).getName() + " " + ((Atom)term.arg(1)).getName(); } }
If we would like to restrict this converter to be applied only to terms subsumed by the term hello(_)
, we could write:
JpcBuilder builder = JpcBuilder.create(); Compound helloCompound = new Compound("hello", asList(Var.ANONYMOUS_VAR)); builder.register(new HelloConverter(), helloCompound); Jpc jpc = builder.build();
Below a usage example of this custom conversion context:
Compound helloWorldCompound = new Compound("hello", asList(new Atom("world"))); assertEquals("hello world", jpc.fromTerm(helloWorldCompound));
The High-Level Querying API
JPC provides features that abstract programmers from explicit conversions from and to Prolog terms.
Below an example of an alternative implementation of the Station
interface that makes use of such high-level API.
public class StationHLApi implements Station { public static final String STATION_FUNCTOR_NAME = "station"; private final String name; private final PrologEngine prologEngine; public StationHLApi(String name) { this.name = name; prologEngine = getPrologEngine(getClass()); } @Override public Station connected(Line line) { String stationVarName = "Station"; Term message = jpc.compound("connected", asList(new Var(stationVarName), line)); Term objectMessage = jpc.compound("::", asList(this, message)); Query query = getPrologEngine().query(objectMessage, jpc); return query.<Station>selectObject(stationVarName).oneSolution(); } @Override public List<Station> nearby() { String stationVarName = "Station"; Term message = new Compound("nearby", asList(new Var(stationVarName))); Term objectMessage = jpc.compound("::", asList(this, message)); Query query = getPrologEngine().query(objectMessage, jpc); return query.<Station>selectObject(stationVarName).allSolutions(); } ... // other methods declared in the Station interface. }
In the previous implementation, we assume a custom Jpc context that knows how to convert between instances of Station
and Prolog terms.
By means of this context, the code of the class is not polluted with explicit inter-language conversion operations.
For example, on line 19 the expression query.<Station>selectObject(stationVarName).oneSolution()
returns the transformation to a Java object of the term that has been bound to the logic variable stationVarName
in the first solution to the query.
Similarly, on line 28 the operation of gathering all the solutions to the query and transforming them to a Java list of stations is accomplished in just one line of code.
Other Features
There are several other JPC's features that have not been discussed here. Some examples are related to inter-language exception handling, preservation of object identity in conversions, and the Prolog-side JPC's API. These and other features will be discussed in a future version of this tutorial.