Below is a review of some of the decisions made in the development of Projog.
Java
Projog is an implementation of the Prolog logic programming language for the Java Virtual Machine (JVM). There are a number of programming languages that target the JVM - e.g. Clojure, Groovy, JRuby, Jython and Kotlin. The benefits of a programming language targeting the JVM include:
- "JVM has established track record and trust level" (Source: Clojure Rationale)
- Runs on many operating systems and hardware types.
- Automatic garbage collection.
- Large collection of libraries and tools.
Inversion of Control
The configuration of built-in predicates and arithmetic operators is configured using dependency injection, a form of Inversion of Control. The projog-bootstrap.pl file contains rules and facts that are executed when a KnowledgeBase
is created. This dependency injection mechanism makes it possible to extend Projog with new functionality without having to modify the existing inference engine code. It is possible for developers to specify an alternative to the default projog-bootstrap.pl
file. This allows projects that depend on Projog to be configured with a unique set of built-in predicates specific to the particular problem the project is trying to solve.
A dependency injection approach could of been implemented in Projog using a framework such as Spring or Guice. I feel that, for the limited requirements of Projog, using a dependency injection framework would actually add more complexity than it removed. I did not want the added overhead of maintaining a separate XML file for configuration. The use of annotations for configuration would tightly couple Projog to the framework. If Projog became dependent on any third-party libraries (such as Spring) then there could be the potential for conflicts between the version of the libraries required by Projog and versions used by other parts of any project it was being integrated with.
Numbers
Projog provides two number types - IntegerNumber
and DecimalFraction
. The IntegerNumber
class represents numbers using a long
primitive and the DecimalFraction
class represents numbers using a double
primitive. As numbers in Projog are represented using Java primitive types, calculations that use them are subject to overflow and loss of precision behaviour. e.g.:
?- X is 9223372036854775807 + 1.
X = -9223372036854775808
?- X is 5.6 + 5.8.
X = 11.399999999999999
Possible alternatives to this approach include:
- The static "helper" methods of
Math
- e.g.Math.addExact(long, long)
- could be used to cause an exception to be thrown when an attempted calculation would result in an overflow. - Instead of using primitive types, numbers could be represented using
java.math.BigInteger
andjava.math.BigDecimal
. The use ofBigInteger
andBigDecimal
would allow the representation of arbitrarily large numbers. A disadvantage ofBigInteger
andBigDecimal
, compared to primitive types, is a possible negative impact on performance. - Take a similar approach to Clojure's number support.
ISO Compatabilty
A valid criticism of Projog is that it does not fully conform to the ISO Prolog standard which was designed to promote a common approach to Prolog. If Projog met the standard then it would (in theory) make it easier for existing code, originally developed using other Prolog implementations, to be ported to Projog.
The approach for moving closer towards ISO compatibility has been to add new predicates as the need for them arises. For example, as part of testing Projog against the solutions provided for Ninety-Nine Prolog Problems numerous new predicates were identified and added.
Java Interop
Projog supports interoperation between Prolog and Java. Projog can be used to run Prolog code from Java. Java can be used to extend the functionality Projog makes available from Prolog code.
The Projog
class provides a convenient API for interacting with Projog from Java. The Projog API is documented, proven (the same API is used by the system tests) and familiar (it uses a similar approach to the JDBC API).
The approach for using Java to add new predicates is documented, proven (it is the same mechanism used to add the existing functionality provided by Projog) and allows Projog to be extendable without the need for modification. The addition of new functionality to Projog is aided by:
- A test framework that was specifically created to support the documentation generation and unit testing of all predicates as part of an automated build process.
- A powerful and concise mechanism for configuration.
To aid further integration between Projog and Java a new "Java object" TermType
could be introduced. This would allow Java objects to be used as arguments in Prolog queries - in the same way that atoms, numbers and lists currently are.
Testing
There are two distinct types of tests used in the development of Projog - unit-tests implemented using JUnit and system tests implemented using a Projog-specific syntax.
The unit tests have provided a convenient mechanism for getting quick feedback on the correctness of features as they have been developed. The consistent use of unit testing has led to the development of a sizeable collection of unit tests which will support any future extension or refactoring of the existing code.
While the unit tests are good for testing discrete parts of the code the system tests have the advantage of being:
- Concise - The concise syntax used to construct the system tests make them quick to write and their intent clear.
- Comprehensive - Each system test covers the full execution process including the parsing of Prolog queries, unification, backtracking and the generation of results.
- Consistent - The system test framework uses the Projog API to interact with Projog.
The system test format has provided a convenient way to construct a suite of regression tests. When a bug is identified in Projog the first step towards investigating it has been to construct the most simple system test possible that demonstrates the problem. The tests provide a quick feedback loop to determine the affects of potential fixes to the problem. As these tests are kept in source control and run as part of every build they give confidence that new changes to the code do not cause previously fixed bugs to be reintroduced. This aids productivity and quality, and will be useful during any future refactoring of the codebase. An example of this process is this test which was created for this bug which led to this change.
The built-in predicates (i.e. implementations of PredicateFactory
) that exist in the Projog code base are only tested by system tests, they do not have corresponding unit tests. I feel that the concise nature of the system tests is more suitable for testing the built-in predicates than JUnit tests would be. I think having both system tests and unit tests for each of these items would require a significant effort and duplication without increasing confidence in the code.
Documentation
There are two distinct types of documentation provided by Projog - Javadoc and the website's manual.
Javadoc is an established approach to documenting Java code using comments in the source code. The Projog Javadocs contain links to class diagrams generated by PlantUML. I think diagrams can assist in describing the structure of a system. PlantUML allows diagrams to be expressed in a textual form which, compared to using a graphical tool, has the advantages of:
- Avoiding needing to spend time aligning boxes and arrows.
- Makes it easy to see what has changed between different versions stored in version control.
The Javadoc comments and system tests are used to automatically generate the website's documentation of built-in predicates. For example, the Between.java file gets automatically transformed into the Between.html file as part of the build process. The use of Javadoc comments and system tests, rather than maintaining a seperate set of documentation, promotes consistency and saves time. As the documentation contains the actual output of running the system tests then we can have confidence the documentation provides a correct account of how the code works.
The decoupling of the website content (stored in Javadoc comments and system tests) from the presentation details makes it possible to quickly and consistently redesign the layout, styling and navigation.
The combination of Projog-specific system test syntax, Java source code and Javadoc comments in the same file may be considered by some to be confusing. Robert C. Martin's Clean Code lists Multiple Languages in One Source File as a code smell. I feel that there are benefits to storing the system tests alongside the Java source code of the functionality they test. If the system tests were stored in their own seperate files then there would still need to a be a way to associate them with the Java source code in order to generate the pages of the user manual. Rather than causing confusion or being a distraction, I think the concise format and clear intent of the system tests contributes to the understanding of the Java source file they belong to.
Projog Alternatives
There are a number of different Prolog implementations. An established and free implementation is SWI-Prolog. It has a rich set of features and tools which include JPL (a Java interface) and SWISH (a web based environment).
Prolog is not the only language that can be used for logic programming. Other choices include:
- Datalog is supported by (amongst others) the Apache Jena Semantic Web framework and the Datomic distributed database.
- core.logic is a logic programming library for Clojure and ClojureScript.
- Mercury is a functional logic programming language developed at the University Of Melbourne.