Monolune

Web Programming in SWI Prolog

SWI Prolog has web programming capabilities. This allows one to write web applications and web services (e.g. REST API servers) in Prolog. One can argue that Prolog is not a good fit for web programming. Admittedly, this is a valid concern because of the lack of third-party libraries, and because of the small Prolog community (which of course means that the Prolog web programming community is even smaller). But sometimes, Prolog is the best tool for the job. For example, the web programming libraries of SWI Prolog allows one to create REST API interfaces that take a JSON query from outside the Prolog system, and return the result in JSON format. This allows one to make use of Prolog to solve tasks that it is good at, while being able to write the rest of the application using another language.

In this article, I will provide two simple examples of web programs, one is a web application, while the other is a web service that takes a JSON request and returns a JSON response.

Web Applications in Prolog

Here is a simple application that shows the number of seconds elapsed since the epoch (1970-01-01 00:00:00 UTC). Using a web browser, one will be able to access the application at http://localhost:8000.

:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/html_write)).

% URL handlers.
:- http_handler('/', handle_request, []).

% Request handlers.
handle_request(_Request) :-
    get_time(X),  % X = seconds elapsed since the epoch.
    reply_html_page(
        [title('Hello')],
        [h1('Hello'), p(X)]
    ).

server(Port) :-
    http_server(http_dispatch, [port(Port)]).

:- initialization(server(8000)).

Let's go over the code above.

:- http_handler('/', handle_request, []). specifies how to handle requests that are made to / (i.e. the web root). This line causes the web server to use handle_request/1 to handle requests that are made to /. http_handler/3 comes from the http/http_dispatch library. That is why we imported this library near the beginning of the program.

handle_request/1 receives a HTTP request, and produces the HTTP response. reply_html_page/2 causes the server to send a HTML page as a response to the request. The first argument represents the contents of the HTML <head> tag, while the second argument represents the contents of the HTML <body> tag. reply_html_page/2 is from the http/http_wite library.

server/1 starts a server on the specified port. If given an anonymous variable (i.e. _), it automatically assigns a random unused port. http_server\2 is from the http/thread_httpd library.

The last line, :- initialization(server(8000))., is what actually starts the web server when we run the program.

To run the program above, save it as server.pl and run swipl server.pl. Open your web browser and navigate to http://localhost:8000. You should see a web page containing the number of seconds elapsed since the epoch. Refresh the page and the value should change.

The code above should be a good starting point for a basic web application. In the next section, we will show an example of a web service, followed by some extra tips.

Web Services in Prolog

In this example, we will look at an API web service. Here, the server waits for clients to send a JSON requests of the form {"a": 3, "b": 4}, and the server responds with JSON responses of the form {"answer": 7} (i.e. the sum of a and b).

:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_json)).

% URL handlers.
:- http_handler('/', handle_request, []).

% Calculates a + b.
solve(_{a:X, b:Y}, _{answer:N}) :-
    number(X),
    number(Y),
    N is X + Y.

handle_request(Request) :-
    http_read_json_dict(Request, Query),
    solve(Query, Solution),
    reply_json_dict(Solution).

server(Port) :-
    http_server(http_dispatch, [port(Port)]).

:- initialization(server(8000)).

The program above is similar to the example on serving HTML web pages. Instead of dealing with HTML web pages, this example deals with JSON. As such we make use of the http/http_json library in the place of http/html_write.

Let's take a look at handle_request/1. In it, Query is the dict representation of the JSON data. solve/2 takes that dict representation of JSON and produces Solution, which is also a dict representation of JSON. The server sends Solution as the HTTP response.

To run the program, save the program as json_server.pl, and run swipl json_server.pl. Whenever clients send valid JSON data of the expected form (i.e. {"a": 1, "b": 2}) to the server, the server responds with a JSON response.

You can test out the server through the command line by sending JSON requests to it using curl, wget, httpie, or any client of your choice. For example:

# cURL.
curl --header 'Content-Type: application/json' \
     --request POST \
     --data '{"a": 1, "b": 2 }' \
     'http://localhost:8000'

# Wget.
wget -qO- --header='Content-Type: application/json' \
          --post-data='{"a": 1, "b": 2}' \
          'http://localhost:8000'

# HTTPie.
http :8000 a:=1 b:=2

Bonus

Logging

To log requests, use the http/http_log module (i.e. :- use_module(library(http/http_log)).). Using this module will, by default, write logs to a file named httpd.log in the directory from which the server was started. To change the name or location of the log file:

% Option 1: relative path.
:- set_setting(http:logfile, 'my_log_file.log').

% Option 2: absolute path.
:- set_setting(http:logfile, '/home/me/my-logs/web.log').

% Option 3: to stdout.
:- set_setting(http:logfile, '/dev/stdout').

Logging to stdout means that the logs will appear in the interactive top level (i.e. the REPL) instead of in a file. To be able to log to standard output, make sure that the /dev/stdout file is actually present on your system, otherwise the method shown would not work. Example use case of logging to standard output: logging to standard output is useful when running SWI Prolog web applications inside Docker containers, because logging to stdout allows the inspection of logs using docker logs.

Exit If Cannot Allocate Port

By default, if you try to start the web server on a port that has already been allocated to another process, the web server will not start. This makes sense. However, SWI Prolog does not exit. To make SWI Prolog quit right after failing to allocate a port for the web server, change the initialization line of the program to:

:- initialization(server(8000), program).

In the example above, if server(8000). fails, SWI Prolog will exit with an error code.

Conclusion

At times, SWI Prolog's official documentation can be a tough read mainly because of the lack of examples. I hope I have provided an appropriate mental starting point from which you can build your own web programming projects.