Architecture Considerations for Prolog
WHAT IF? What if you had to change some major part of your project? How much else would have to change? One of the great benefits of well designed architecture is that you can plug and play, swapping out parts as needed. Let’s walk through a generic software design, starting from the data to the user interface and consider what might change and how we should design our code.
My motivation for this article is two-fold: hopefully it will provide some insight for academics who provide great contributions to Prolog but may have less experience writing complex applications, to understand the requirements of a software developer. Also, it aims to provide some insight for Prolog developers who find themselves without the usual frameworks/templates guiding their architecture. Because I believe the tidbits and considerations in the text are as important as the architecture, there’s no pictures!
Data
THESE ARE your Prolog facts, popular representations include frames and triples, but can also be dicts, RDF, or an external database. When choosing a data representation it must be both easy for a machine to read for efficiency, as well as easy for a human to read so they can validate it and explore it.
Some data stores require processing from a human readable version to a machine readable one, providing most readability for each end-user. However, this also decouples what the human reads from what the machine does, so the human cannot be sure that the machine is receiving the data they intended. At best, this is a debugging nightmare, at worst it’s an application that doesn’t behave as expected.
frame(subject, [attr1=value1, attr2=value2]).
triple(subject, attr1, value1).
triple(subject, attr2, value2).
A good architecture should allow us to substitute any of these data stores for another and the application not know. To allow this substitution requires two things: a proxy and a non-intelligent data store. Data storage systems like frames were designed to run daemon processes, allowing values to be inferred on read. This was very clever, but poor architecture because it will prohibit changing to say RDF if I wanted to share my data. The proxy makes substitution easy, in this example you could switch from frames to triples by changing 3 lines:
:- module(data_proxy, [get_data/3]).
:- use_module(frames, [get_frame/2]).
% :- use_module(triples,[get_triple/3 as get_data]).
get_data(Subject, Predicate, Object) :-
get_frame(Subject, Predicate=Object).
Inference
ONCE WE’VE got the data, we usually need to employ some reasoning or calculating to get new data. Again, there are choices here, concerning which logic is used and also which language. You could use Prolog, Datalog, SWRL, or even pass it off to a C function using the foreign language interface. You might also consider calculating these on write to improve read times over update times. As you can see, there are many options here, that all have their own benefits and you may wish to change methods overtime to improve performance, efficiency or capability. For this reason, you want to make your current chosen method easy to substitute.
Again, we use a proxy for this:
:- module(inference_proxy, [infer/3]).
:- use_module(rules, [rule/3 as infer/3]).
Whatever you choose to implement rule/3
with, it shouldn’t know if you’re using frames, triples, RDF or whatever else to store your data. All it should know is your data_proxy
module so if the data storage is changed, your rules still work.
A Querying Façade
A FAÇADE is not strictly necessary, but it makes a lot of sense. From the point of view of the application, it doesn’t care if the data it needs was recorded data, or inferred. By placing a querying façade between the application and the data it becomes easier to co-ordinate the components of the software architecture. Furthermore, the software developers don’t need to remember which facts are recorded and which are inferred, which could also change overtime.
:- module(query, [query/3]).
:- use_module(data_proxy, [get_data/3]).
:- use_module(inference_proxy, [infer/3]).
query(Subject, Predicate, Object) :-
get_data(Subject, Predicate, Object).
query(Subject, Predicate, Object) :-
infer(Subject, Predicate, Object).
The Core Of The Application
THERE’S A reason we’re making this application, and this is where we encode it, traditionally called the “Business Rules”. Usually these are defining the things (objects?) and behaviours that are required for the user. Important: They should not contain any presentation information, that’s for the UI end. By using the query façade and proxies, we’ve protected these rules from change in the data end, business rules are protected from changes in the UI by also not knowing about it. Some example applications and possible objects:
- Weather Application:
- Forecast
- Current Weather In
- Teaching Platform:
- Lesson
- Account
- Exam
- Auto-Marker
- Vehicle Diagnostics
- Diagnostic
- Report
These things should provide a versatile, manipulable representation of what they represent and encapsulate their methods so change in one place does not cause issues elsewhere. For this you can make use of Prolog’s module system, which provides encapsulation, or you can take full advantage of OO design with Logtalk.
UI
THE UI is probably the most volatile aspect of the software design, with more changes here than anywhere else. Let’s adapt a favourite design of web applications: model-view-controller. Consider your business rules as your model, you can query them and get the objects of your application. The controller receives queries from the user and co-ordinates the objects required for the response. The view will show it to them.
For the UI we consider the substitution principal from the view, backwards. A well designed architecture makes it easy to add a new UI, such as a VUI, or web app/ mobile app/ desktop app. Therefore your view should be the only thing that knows about the existence of the library you’re using to make that UI, but it won’t quite workout that way because each library will require some customisation of the data being passed to it. For this reason, you want to split your controller into two parts.
Part 1 of the controller works with the business rules, so you have the data you need with no UI information. When working with Prolog I like to get my data into dicts at this point so it’s easy to turn to JSON. I make use of the dict tab: tab{key: value}
to encode type information for later.
Part 2 of the controller adds data required for the UI, this is stuff like colour, position, sizing etc. Having the tab of the dict encode the type is very useful here, along with the maplist
predicates. This keeps the substitution principal working as a change in a UI framework only requires changes to the view and this controller.
Conclusion
Software architecture is as complex as the application being designed. It’s possible to merge layers for simple applications, but when developing anything serious, these are some of the considerations a software developer has to take into account. We’ve only scratched the surface in this post, but I believe if you design your software to make it easy to substitute components in and out, you’ll be off to a very good start.
*[UI]: User Interface *[OO]: Object-Oriented *RDF: Resource Description Framework *[JSON]: JavaScript Object Notation *[VUI]: Voice User Interface