JPC User Guide

(Work in progress).
Contents

    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.

    Named categorization for engines.
    Fig 1. - A named categorization for engines.

    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:

    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.

    State diagram chart of a JPC query.
    Fig 2. - State diagram chart of a JPC query.

    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:

    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.

    The JPC Context.
    Fig 3. - The JPC Context.

    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.