2018-09-24
Concurrent and Distributed Programming in Web Prolog
SWI Prolog is an open source Prolog implementation with good documentation and many interesting libraries. One library that was published recently is Web Prolog. While branded as a 'Web Logic Programming Language' the implementation in the github repository runs under SWI Prolog and adds Erlang style distributed concurrency. There is a PDF Book in the repository that goes into detail about the system. In this post I explore some of features.
Install SWI Prolog
The first step is to install and run the SWI Prolog development version. There is documentation on this but the following are the basic steps to download and build from the SWI Prolog github repository:
$ git clone https://github.com/SWI-Prolog/swipl-devel
$ cd swipl-devel
$ ./prepare
... answer yes to the various prompts ...
$ cp build.tmpl build
$ vim build
...change PREFIX to where you want to install...
$ make install
$ export PATH=<install location>/bin:$PATH
There may be errors about building optional libraries. They can be ignored or view dependancies to see how to install.
The newly installed SWI Prolog build can be run with:
$ swipl
Welcome to SWI-Prolog (threaded, 64 bits, version 7.7.19-47-g4c3d70a09)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.
For online help and background, visit http://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).
?-
I recommend reading Basic Concepts in The Power of Prolog to get a grasp of Prolog syntax if you're not familiar with it.
Starting a Web Prolog node
Installing the Web Prolog libraries involves cloning the github repository and loading the prolog libraries held within. The system includes a web based tutorial and IDE where each connected web page is a web prolog node that can send and receive messages to other web prolog nodes. I'll beiefly mention this later but won't cover it in detail in this post - for now some simple examples will just use the SWI Prolog REPL. We'll need two nodes running, which can be done by running on different machines. On each machine run:
$ git clone https://github.com/Web-Prolog/swi-web-prolog/
$ cd swi-web-prolog
$ swipl
...
?- [web_prolog].
...
The [web_prolog].
command at the prolog prompt loads the web prolog libraries. Once loaded we need to instantiate a node, which starts the socket server and machinery that handles messaging:
?- node.
% Started server at http://localhost:3060/
true.
This starts a node on the default port 3060
. If you're running multiple nodes on the same machine you can change the port by passing an argument to node
:
?- node(localhost:3061).
% Started server at http://localhost:3061/
true.
In the rest of this post I refer to node A
and node B
for the two nodes started above.
Basic example
Each process has an id that can be used to reference it. It is obtained via self
:
?- self(Self).
Self = thread(1)@'http://localhost:3061'.
We can send a message using '!
'. Like Erlang, it takes a process to send to and the data to send:
?- self(Self),
Self ! 'Hello World'.
This posts 'Hello World' to the current process mailbox. Messages can be received using receive
. This takes a series of patterns to match against and code to run if an item in the process mailbox matches that pattern. Like Erlang it is a selective receive - it will look through messages in the mailbox that match the pattern, not just the topmost message:
?- receive({ M -> writeln(M) }).
Hello World
M = 'Hello World'.
Notice the receive
rules are enclosed in '{ ... }
' and are comprised of a pattern to match against and the code to run separated by ->
. Variables in the pattern are assigned the value of what they match. In this case M
is set to Hello World
as that's the message we sent earlier. More complex patterns are possible:
?- $Self ! foo(1, 2, bar('hi', [ 'a', 'b', 'c' ])).
Self = thread(1)@'http://localhost:3061'.
?- receive({ foo(A, B, bar(C, [D | E])) -> true }).
A = 1,
B = 2,
C = hi,
D = a,
E = [b, c].
Here I make use of a feature of the SWI Prolog REPL - prefixing a variable with $
will use the value that was assigned to that variable in a previous REPL command. This also shows matching on the head and tail of a list with the [Head | Tail]
syntax.
To send to another process we just need to use the process identifier. In this case I can send a message from node A to node B:
# In Node A
?- self(Self).
Self = thread(1)@'http://localhost:3060'.
# This call blocks
?- receive({ M -> format('I got: ~s~n', [M])}).
# In Node B
?- thread(1)@'http://localhost:3060' ! 'Hi World'.
true.
# Node A unblocks
I got: Hi World
M = 'Hi World'.
Spawning processes
Use spawn
to spawn a new process the runs concurrencly on a node. It will have its own process Id:
?- spawn((self(Self),
format('My process id is ~w~n', [Self]))).
true.
My process id is 21421552@http://localhost:3060
The code to run in the process is passed as the first argument to spawn
and must be between brackets. An optional second argument will provide the process id to the caller:
?- spawn((self(Self),
format('My process id is ~w~n', [Self])),
Pid).
My process id is 33869438@http://localhost:3060
Pid = '33869438'.
A third argument can be provided to pass options to control spawning. These include the ability to link processes, monitor them or register a name to identify it in a registry.
Using the REPL we can define new rules and run them in processes, either by consulting a file, or entering them via [user]
. The following code will define the code for a 'ping' server process:
server :-
receive({
ping(Pid) -> Pid ! pong, server
}).
This can be added to a file and loaded in the REPL with consult
, or it can be entered directly using [user]
(note the use of Ctrl+D to exit the user input):
# on Node A
?- [user].
|: server :-
|: receive({
|: ping(Pid) -> Pid ! pong, server
|: }).
|: ^D
% user://1 compiled 0.01 sec, 1 clauses
true.
server
blocks receiving messages looking for a ping(Pid)
message. It sends a pong
message back to the process id it extracted from the message and calls itself recursively using a tail call. Run on the node with:
# on node A
?- spawn((server), Pid).
Pid = '18268992'.
Send a message from another node by referencing the Pid along with the nodes address:
# on node B
?- self(Self), 18268992@'http://localhost:3060' ! ping(Self).
Self = thread(1)@'http://localhost:30601.
?- flush.
% Got pong
true.
The flush
call will remove all messages from the process' queue and display them. In this case, the pong
reply from the other nodes server.
Spawning on remote nodes
Web Prolog provides the ability to spawn processes to run on remote nodes. This is done by passing a node
option to spawn
. To demonstrate, enter the following to create an add
rule in node A:
# on node A
?- [user].
|: add(X,Y,Z) :- Z is X + Y.
|: ^D
% user://1 compiled 0.01 sec, 1 clauses
true.
This creates an 'add' predicate that works as follows:
# on node A
?- add(1,2,Z).
Z = 3.
We can call this predicate from node B by spawning a remote process that executes code in node A. In node B, it looks like this:
# on node B
?- self(Self),
spawn((
call(add(10, 20, Z)),
Self ! Z
), Pid, [
node('http://localhost:3060'),
monitor(true)
]),
receive({ M -> format('Result is ~w~n', [M])})
Result is 30
Self = thread(1)@'http://localhost:3061',
Pid = '24931627'@'http://localhost:3060',
M = 30.
Passing node
as an option to spawn/3
results in a process being spawned on that referenced node and the code runs there. In the code we call the add
predicate that only exists on node A and return it by sending it to node B's identifier. The monitor
option provides status information about the remote process - including getting a message when the process exits. Calling flush
on node B shows that the process on node A exits:
# on Node B
?- flush.
% Got down('24931627'@'http://localhost:3060',exit)
true.
Note the indirection in the spawn
call where add
isn't called directly, instead call
is used to call it. For some reason Web Prolog requires add
to exist on node B even though it is not called. The following gives an error on node B for example:
# on node B
?- self(Self),
spawn((
add(10, 20, Z),
Self ! Z
), Pid, [
node('http://localhost:3060'),
monitor(true)
]),
receive({ M -> format('Result is ~w~n', [M])})
ERROR: Undefined procedure: add/3 (DWIM could not correct goal)
I'm unsure if this is an error in web prolog or something I'm doing.
RPC calls
The previous example can be simplified using RPC functions provided by web prolog. A synchronous RPC call from node B can be done that is aproximately equivalent to the above:
?- rpc('http://localhost:3060', add(1,2,Z)).
Z = 3.
Or ansynchronously using promises:
?- promise('http://localhost:3060', add(1,2,Z), Ref).
Ref = '119435902516515459454946972679206500389'.
?- yield($Ref, Answer).
Answer = success(anonymous, [add(1, 2, 3)], false),
Ref = '119435902516515459454946972679206500389'.
Upgrading a process
The following example, based on one I did for Wasp Lisp, is a process that maintains a count that can be incremented, decremented or retrieved:
server(Count) :-
receive({
inc -> Count2 is Count + 1, server(Count2);
dec -> Count2 is Count - 1, server(Count2);
get(Pid) -> Pid ! value(Count), server(Count)
}).
Once defined, it can be spawned in node A with:
?- spawn((server(0)), Pid).
Pid = '33403644'.
And called from Node B with code like:
?- self(Self), 33403644@'http://localhost:3060' ! get(Self).
Self = thread(1)@'http://localhost:3061'.
?- flush.
% Got value(0)
true.
?- 33403644@'http://localhost:3060' ! inc.
true.
?- self(Self), 33403644@'http://localhost:3060' ! get(Self).
Self = thread(1)@'http://localhost:3061'.
?- flush.
% Got value(1)
true.
We can modify the server so it accepts an upgrade
message that contains the new code that the server will execute:
server(Count) :-
writeln("server receive"),
receive({
inc -> Count2 is Count + 1, server(Count2);
dec -> Count2 is Count - 1, server(Count2);
get(Pid) -> Pid ! value(Count), server(Count);
upgrade(Pred) -> writeln('upgrading...'), call(Pred, Count)
}).
Run the server and send it some messages. Then define a new_server
on node A's REPL:
new_server(Count) :-
writeln("new_server receive"),
receive({
inc -> Count2 is Count + 1, new_server(Count2);
dec -> Count2 is Count - 1, new_server(Count2);
mul(X) -> Count2 is Count * X, new_server(Count2);
get(Pid) -> Pid ! value(Count), new_server(Count);
upgrade(Pred) -> writeln(Pred), writeln(Count), call(Pred, Count)
}).
And send an upgrade
message to the existing server:
# on node A
?- $Pid ! upgrade(new_server).
upgrading...
new_server receive
Now the server understands the mul
message without having to be shut down and restarted.
Web IDE and tutorial
There's a web based IDE and tutorial based on SWISH, the online SWI-Prolog system. To start this on a local node, run the following script from the web prolog source:
$ cd web-client
$ swipl run.pl
Now visit http://localhost:3060/apps/swish/index.html
in a web browser. This displays a tutorial on the left and a REPL on the right. Each loaded web page has its own namespace and local environment. You can send messages to and from individual web pages and the processes running in the local node. It uses websockets to send data to and from instances.
Conclusion
There's a lot to the Web Prolog system and the implementation for SWI Prolog has some rough edges but it's usable to experiment with and get a feel for the system. I hope to write a more about some of the features of it, and SWI Prolog, as I get more familiar with it.