Bluish Coder

Programming Languages, Martials Arts and Computers. The Weblog of Chris Double.


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.

Tags


This site is accessable over tor as hidden service mh7mkfvezts5j6yu.onion, or Freenet using key:
USK@1ORdIvjL2H1bZblJcP8hu2LjjKtVB-rVzp8mLty~5N4,8hL85otZBbq0geDsSKkBK4sKESL2SrNVecFZz9NxGVQ,AQACAAE/bluishcoder/-44/


Tags

Archives
Links