While teaching myself how to develop a web application with SWI-Prolog, I’ve taken notes for future reference, which others may find useful since I’ve had to figure much of this out through trial and error.
My tutorial differs from Anne Ogborn’s at http://www.pathwayslms.com/swipltuts/html/index.html in that I’ve not used the html_write library (I’m ideologically opposed to templating), and have gone in directions I personally found initially difficult to grasp from the documentation (though which turned out to be very simple once I went aha!).
I’ve put my progress so far here for any suggestions or corrections. I don’t think it’s suitable for https://swish.swi-prolog.org since I assume one can’t run server code on it, so any suggestions for a good place to make it available once complete would be appreciated.
Step 1: Creating a simple SWI-Prolog web server
:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_unix_daemon)).
:- use_module(library(http/http_files)).
:- multifile http:location/3.
http:location(files, root(files), []).
user:file_search_path(folders, library('images/styles/scripts')).
:- http_handler(root(.), http_reply_from_files('.', []), [prefix]). % defaults to index.html
:- http_handler(files(.), serve_files_in_directory(folders), [prefix]).
:- initialization http_daemon.
The lines of prolog above in a file called server.pl create a web server which can be started with, say:
swipl server.pl --port=3030 --pidfile=http.pid
and stopped with:
kill $(cat http.pid)
Assuming there is an index.html file in the same directory as server.pl and its associated files are in subdirectories called images, styles, and scripts, simply point your browser to http://localhost:3030/
One of the many advantages of serving static files rather than programmatically generated output is you can start with a skeleton index.html file,
say:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Page Title</title>
<link rel="stylesheet" href="/styles/mystyle.css">
</head>
<body>
<p>Hello World!</p>
<script src="/scripts/myscript.js"></script>
</body>
</html>
and then see your site develop by simply refreshing your browser as you edit and add files without needing to constantly start and stop the server —
unless you add handlers by editing in the server.pl file, as in the next section.
If you’re new to html coding or need a refresher, I suggest Mozilla’s
Getting started with the Web.
Step 2: Advancing to AJAX
Since I last worked through developing a simple web application, the Javascript world has moved on a lot, with fetch simplifying the verbose syntax still used in
Mozilla’s getting started with Ajax tutorial.
I’ve added a handler linked to url /test in server.pl called test_handler which receives a Json object ‘{“userName”: some value}’.
Using library(http/http_json)'s http_read_json_dict/2, the value of userName is read, prefixed with "Hello, " and then returned housed in a Json object with key computedString.
This is hopefully sufficient to demonstrate how simple it is to use Ajax and Json with a SWI-Prolog server.
:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_unix_daemon)).
:- use_module(library(http/http_files)).
:- use_module(library(http/http_json)).
:- multifile http:location/3.
http:location(files, root(files), []).
user:file_search_path(folders, library('images/styles/scripts')).
:- http_handler(root(.), http_reply_from_files('.', []), [prefix]). % defaults to index.html
:- http_handler(files(.), serve_files_in_directory(folders), [prefix]).
:- http_handler('/test', test_handler, []).
test_handler(Request) :-
member(method(post), Request), !,
http_read_json_dict(Request, _{userName:NameIn}),
string_concat("Hello, ", NameIn, NameOut),
reply_json_dict(_{computedString:NameOut}).
:- initialization http_daemon.
I’ve updated Mozilla’s beginner example to use fetch
instead of XMLHttpRequest in a file called main.js in the scripts subdirectory:
(function() {
document.getElementById("ajaxButton").onclick = function() {
var userName = document.getElementById("ajaxTextbox").value;
makeRequest('/test', userName);
};
function makeRequest(url, userName) {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({"userName": userName})
})
.then(res => res.json())
.then(response => alert(response.computedString))
.catch(error => alert('Caught Exception: ' + error.description));
}
})();
and the index.html file looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Ajax 1</title>
<link rel="stylesheet" href="/styles/basic.css">
</head>
<body>
<label>Your name:
<input type="text" id="ajaxTextbox" />
</label>
<span id="ajaxButton" style="cursor: pointer; text-decoration: underline">
Make a request
</span>
<script src="/scripts/main.js"></script>
</body>
</html>
Step 3: Linking the server to a database (Postgresql in this example).
This is still work in progress. For the impatient, here’s the start of the next
section:
SWI-Prolog communicates with Postgres via the ODBC Interface.
Getting this to work requires having the PostgreSQL ODBC driver installed and an ~/.odbc.ini file. Details are at http://www.unixodbc.org/odbcinst.html
NOTE: MaxVarcharSize = 500 (or bigger) is needed, else ODBC truncates strings at 255 as explained at https://odbc.postgresql.org/docs/config.html
[postgresql]
Description = Test to Postgres
Driver = /usr/lib/psqlodbcw.so
Trace = Yes
TraceFile = sql.log
Database = games
Servername = localhost
UserName = my_username
Password = my_password
Port = 5432
Protocol = 9.4
ReadOnly = No
RowVersioning = No
ShowSystemTables = No
ShowOidColumn = No
FakeOidIndex = No
ConnSettings =
MaxVarcharSize = 500
Queries via SWI-Prolog can then be done like so:
/*
roblaing=> select * from accounts;
owner | balance | amount
-------+---------+--------
Mary | 214.00 |
Bob | 86.00 |
*/
:- use_module(library(odbc)).
dbquery(Owners) :-
odbc_connect('postgresql' , Connection, []),
odbc_query(Connection, 'SELECT owner FROM accounts', Owners, [findall(Owner, row(Owner)]),
odbc_disconnect(Connection).