Web Prolog Tutorial

Getting started

Introduction

This tutorial walks you through Web Prolog step by step. It opens with ordinary Prolog to set the scene, then enters the world of message-passing concurrency: pids, mailboxes, spawning actors, monitoring and linking them, selective receive, guards and timeouts. From those primitives it builds up small idiomatic services — a count server, a fridge simulation, actors playing ping-pong — and shows how to hide their protocols behind tidy client-side helpers.

The middle of the tutorial takes the same primitives across the wire. Toplevels become long-running session actors that you can drive with calling goals, templates, paging, and explicit output and input. rpc/2-3 runs goals on a remote node and streams back proof trees. Spawning an actor with the node option puts it on another node and gives you back a pid you can talk to as if it were local.

The last part introduces higher-level patterns. The server behaviour wraps a callback predicate in a generic supervised process that supports synchronous request/reply, hot code swapping, and automatic recovery from crashes. Statechart actors layer hierarchical state machines on top of the same actor model. A closing chapter exercises Web Prolog's HTTP and WebSocket APIs directly, so you can see exactly what the portal is doing under the hood.

If you are reading this inside the portal, use the terminal to the right. Queries must end with a full stop. If a query has more than one answer, press ; or the space bar for more, or use the green Next button.

Tip! The portal also has a Logger panel to the right of the terminal. If it is hidden, open it with the chevron at the far right. By ticking the API checkbox at the bottom of the Logger, you can watch the HTTP traffic generated by your queries.

append/3 in two modes

Prolog is a relational language. The same predicate can often be used in more than one direction. First, use append/3 in the ordinary way to join two lists:

?- append([a],[b,c],Xs).

Now run the same predicate backwards and let Prolog split a list in all possible ways:

?- append(Xs,Ys,[a,b,c]).

This second query has several answers. Again, press ; or the space bar for more, or use the green Next button.

A small family program

This node hosts some shared Prolog code. The small family program below is part of that shared database, so you can query it directly.

ancestor_descendant(X, Y) :-
    parent_child(X, Y).
ancestor_descendant(X, Z) :-
    parent_child(X, Y),
    ancestor_descendant(Y, Z).

parent_child(X, Y) :-
    mother_child(X, Y).
parent_child(X, Y) :-
    father_child(X, Y).

mother_child(trude, sally).

father_child(tom, sally).
father_child(tom, erica).
father_child(mike, tom).

Ask for Mike's descendants:

?- ancestor_descendant(mike, Who).

Tip! The family program, like many other programs in this tutorial, can also be accessed from the list of Web Prolog source code in the Examples drawer. Clicking on a file name will place the code in the editor and allow you to edit and run it.

Parsing with a DCG

This node also hosts a small grammar written as a Definite Clause Grammar (DCG). You can query it directly to parse or generate sentences.

s(s(NP,VP)) --> np(NP, Num), vp(VP, Num).

np(NP, Num) --> pn(NP, Num).
np(np(Det,N), Num) --> det(Det, Num), n(N, Num).
np(np(Det,N,PP), Num) --> det(Det, Num), n(N, Num), pp(PP).

vp(vp(V,NP), Num) --> v(V, Num), np(NP, _).
vp(vp(V,NP,PP), Num) --> v(V, Num), np(NP, _), pp(PP).

pp(pp(P,NP)) --> p(P), np(NP, _).

det(det(a), sg) --> [a].
det(det(the), _) --> [the].

pn(pn(john), sg) --> [john].

n(n(man), sg) --> [man].
n(n(men), pl) --> [men].
n(n(telescope), sg) --> [telescope].

v(v(sees), sg) --> [sees].
v(v(see), pl) --> [see].
v(v(saw), _) --> [saw].

p(p(with)) --> [with].

Ask for a parse tree:

?- phrase(s(Tree), [john,saw,a,man,with,a,telescope]).

You can also use the same grammar for generation:

?- forall((between(1,8,N), length(S,N), phrase(s(_),S)), writeln(S)).

Message-passing concurrency

Web Prolog extends Prolog with Erlang-style message-passing concurrency. The terminal session to the right is itself an actor with a mailbox. We will use it to send messages, inspect the mailbox, spawn other actors, and watch what happens when actors terminate.

Tip! The session is persistent. Run the examples in order: later queries use shell substitutions such as $Self and $Pid, which stand for the most recent bindings of those variables. If you want to inspect the WebSocket traffic, open the Logger and tick API.

Your shell has a pid

The terminal to the right is running a Web Prolog shell. The core of a shell is a toplevel actor. Start by asking the shell for its process identifier (pid for short):

?- self(Self).

A pid is the address of an actor. If you know a pid, you can send that actor a message.

Send a message to yourself

The shell understands dollar notation: $Self means "use the most recent value of Self". Send the shell a message:

?- $Self ! hello.

Now receive the oldest matching message from the mailbox:

?- receive({Message -> true}).

You should get Message = hello. If the mailbox is empty, receive/1 waits until a matching message arrives.

Inspect the mailbox without blocking

flush/0 is a shell utility: it prints every pending message and never blocks.

?- $Self ! hello, $Self ! goodbye.

?- flush.

This is usually the quickest way to see what is sitting in your mailbox while you are experimenting.

Spawn an actor and talk to it

An actor is capable of spawning other actors and then talking to them. This node hosts a tiny echo actor:

echo_actor :-
    receive({
        echo(From, Msg) ->
            From ! echo(Msg),
            echo_actor
    }).

Spawn it:

?- spawn(echo_actor, Pid).

Then send it a request. The reply comes back asynchronously to your shell mailbox:

?- self(Self), $Pid ! echo(Self, hi).

?- flush.

Monitors tell you when an actor dies

If you spawn with monitor(true), the child sends a down/3 notification when it terminates:

?- spawn(2 > 1, Pid, [monitor(true)]).

The notification lands in the shell mailbox, so inspect it with flush/0:

?- flush.

The message has the form down(Ref, Pid, Reason). Here the actor succeeds normally, so the reason should be true.

Names are easier to remember than pids

You can register a pid under a name and use the name instead of the pid. Here the shell registers itself under the name shell:

?- self(Self), register(shell, Self).

Tip! It is good to know that you can also unregister an actor by calling unregister/1 with its name.
?- shell ! hello.

?- spawn(shell ! goodbye).

?- flush.

Kill an actor from the outside

Some actors do not stop on their own. Here we spawn a monitored echo actor, register it under a name, and then terminate it with exit/2:

?- spawn(echo_actor, Pid, [monitor(true)]),
   register(echo_actor, Pid).

?- whereis(echo_actor, Pid),
   exit(Pid, 'We changed our minds!').

Because the actor was monitored, the mailbox now contains a down(...) message with the exit reason.

?- flush.

Selective receive

receive/1 does not have to take the oldest message. It scans the mailbox for the first message that matches one of its receive clauses and leaves the others in place. This example insists on handling hello before goodbye even if goodbye arrives first.

wait_hello :-
    receive({
        hello ->
            writeln('Got hello.'),
            wait_goodbye
    }).

wait_goodbye :-
    receive({
        goodbye ->
            writeln('Got goodbye.')
    }).

?- self(Self),
   Self ! goodbye,
   Self ! hello,
   wait_hello.

The first receive/1 skips goodbye, consumes hello, and the second receive/1 then picks up the deferred message. This selective behavior is one of the core ideas in Erlang-style message passing.

Receive can do more than one thing

A receive call can have several clauses. The first matching clause wins, and unmatched messages are left in the mailbox for later.

?- self(Self),
   Self ! goodbye(bob),
   receive({
       hello(Name) ->
           format("Hello, ~w!~n", [Name]) ;
       goodbye(Name) ->
           format("Goodbye, ~w!~n", [Name])
   }).

Guards and timeouts

Receive clauses can also have guards. Here the same pattern is handled in two different ways depending on the number inside the message.

?- self(Self),
   Self ! number(-2),
   receive({
       number(N) if N > 0 ->
           format("Positive number: ~p~n", [N]) ;
       number(N) if N =< 0 ->
           format("Non-positive number: ~p~n", [N])
   }).

If you do not want to wait forever, add a timeout. This waits one second for a matching message and prints a note if nothing shows up.

?- receive({
       person(Name) ->
           format("Hello, ~w!~n", [Name])
   }, [
       timeout(1),
       on_timeout(writeln('No person arrived.'))
   ]).

More on monitors

monitor(true) is the simple case. You can also install monitors explicitly with monitor/2. Each monitor gets its own reference and therefore its own down/3 message.

?- spawn(sleep(1), Pid),
   monitor(Pid, Ref1),
   monitor(Pid, Ref2),
   sleep(1.2),
   flush.

If you remove the monitor before the child terminates, the down/3 message never arrives.

?- spawn(sleep(1), Pid),
   monitor(Pid, Ref),
   demonitor(Ref),
   receive({
       down(_, Pid, _) ->
           writeln('Unexpected down message.')
   }, [
       timeout(1.2),
       on_timeout(writeln('No down message arrived.'))
   ]).

Tip! If a child may finish immediately, prefer monitor(true) on spawn/3. It installs the monitor atomically with process creation and avoids the race where the child exits before a later call to monitor/2 has been set up.

Linking controls child lifetime

By default, a child is linked to its parent. If the parent exits, linked children are terminated too:

Now inspect the actors visible from your current session:

The inner nonterminating actor does not survive as a new long-lived child because linking ensures that when the outer spawned actor terminates, its child is taken down with it.

Linking is one-way. A child that exits does not kill its parent:

You should now see one extra live actor: the parent is still there even though its child exited immediately. Clean it up before moving on:

An actor can choose its own exit reason

A monitored actor can stop itself with exit/1. The reason ends up in the down/3 message:

?- spawn(exit(my_reason), Pid, [monitor(true)]).

?- flush.

Sending to a dead pid is just a no-op, so monitors are the usual way to learn that an actor has gone away.

A small priority queue

This example, borrowed from Fred Hébert's Learn You Some Erlang for Great Good!, uses guards plus zero-timeout receives to sort "important" messages before normal ones. First load the predicates into your current session:

important(Messages) :-
    receive({
        Priority-Message if Priority > 10 ->
            Messages = [Message|MoreMessages],
            important(MoreMessages)
    }, [
        timeout(0),
        on_timeout(normal(Messages))
    ]).

normal(Messages) :-
    receive({
        _-Message ->
            Messages = [Message|MoreMessages],
            normal(MoreMessages)
    }, [
        timeout(0),
        on_timeout(Messages = [])
    ]).

Now send four messages to yourself:

?- self(S),
   S ! 15-high,
   S ! 7-low,
   S ! 1-low,
   S ! 17-high.

Then ask the queue to collect them in priority order:

?- important(Messages).

The result should be Messages = [high,high,low,low]. This works only because unmatched messages are deferred rather than discarded.

Programming with actors

I would prefer multi-threading in Prolog to look as much as possible like Erlang.

Richard O'Keefe

These examples make one point clear: Web Prolog actors do not just borrow Erlang's vocabulary. If you program them in the Erlang style, they behave that way too.

A count server

This actor keeps its state in an argument. Each count(From) message increments the counter and sends the new count back. A stop message terminates the server cleanly.

count_actor(Count0) :-
    receive({
        count(From) ->
            Count is Count0 + 1,
            From ! count(Count),
            count_actor(Count) ;
        stop ->
            true
    }).

?- spawn(count_actor(0), Pid, [
       monitor(true)
   ]).

?- self(Self).

?- $Pid ! count($Self).

?- receive({Count -> true}).

?- $Pid ! count($Self), $Pid ! stop.

?- flush.

Because the actor was monitored, the final flush/0 should show both the updated count and a down(...) message.

A fridge simulation

The following example, also adapted from Fred Hébert's Learn You Some Erlang for Great Good!, simulates a fridge that keeps its state as a list of food items. Messages are used to store food, take food, or terminate the server.

fridge(FoodList0) :-
    receive({
        store(From, Food) ->
            self(Self),
            From ! Self-ok,
            fridge([Food|FoodList0]);
        take(From, Food) ->
            self(Self),
            (   select(Food, FoodList0, FoodList)
            ->  From ! Self-ok(Food),
                fridge(FoodList)
            ;   From ! Self-not_found,
                fridge(FoodList0)
            );
        terminate ->
            true
    }).

?- spawn(fridge([]), Pid, [
       monitor(true)
   ]).

?- self(Me), $Pid ! store(Me, meat), $Pid ! store(Me, cheese).

?- flush.

?- self(Me), $Pid ! take(Me, cheese).

?- flush.

?- $Pid ! terminate.

?- flush.

Hiding the protocol details

Raw message sending works, but it forces the client to remember the protocol. The usual fix is to wrap the protocol in ordinary predicates.

store(Pid, Food, Response) :-
    self(Self),
    Pid ! store(Self, Food),
    receive({
        Pid-Response -> true
    }).

take(Pid, Food, Response) :-
    self(Self),
    Pid ! take(Self, Food),
    receive({
        Pid-Response -> true
    }).

Load these helpers, then use them directly:

?- spawn(fridge([]), Pid, [
       monitor(true)
   ]).

?- store($Pid, cheese, Response).

?- take($Pid, cheese, Response).

?- $Pid ! terminate.

?- flush.

This style is easier to read and much easier to reuse. The actor still speaks the same message protocol, but clients no longer need to handle it explicitly.

Delayed sending

Web Prolog supports delayed sending. Here we use it to build a tiny alarm actor.

alarm :-
    receive({
        ring ->
            writeln('Alarm ringing!'),
            alarm;
        stop ->
            true
    }).
?- spawn(alarm, Pid).

?- send($Pid, ring, [
       delay(5),
       id(alarm1)
   ]).

?- cancel(alarm1).

?- $Pid ! stop.

If the cancellation happens in time, the alarm never rings. Send stop afterwards so the actor does not keep waiting in the background.

Ping-pong

This example is adapted from the official Erlang message-passing guide. Two actors send messages back and forth three times, then finish.

ping(0, Pong_Pid) :-
    Pong_Pid ! finished,
    format('Ping finished.~n',[]).
ping(N, Pong_Pid) :-
    self(Self),
    Pong_Pid ! ping(Self),
    receive({
        pong ->
            format('Ping received pong.~n',[])
    }),
    N1 is N - 1,
    ping(N1, Pong_Pid).

pong :-
    receive({
        finished ->
            format('Pong finished.~n',[]) ;
        ping(Ping_Pid) ->
            format('Pong received ping.~n',[]),
            Ping_Pid ! pong,
            pong
    }).

ping_pong :-
    spawn(pong, Pong_Pid),
    spawn(ping(3, Pong_Pid)).

?- ping_pong.

The interesting thing here is not just that the code looks close to Erlang, but that the interaction pattern is the same: send, receive, recurse, terminate.

Programming with toplevels

A shell is itself a toplevel, but Web Prolog also lets one actor spawn another toplevel actor and talk to it explicitly. A spawned toplevel has its own mailbox, its own private Prolog database, and a built-in protocol for calling goals, paging through answers, producing output, prompting for input, and being stopped or halted.

The shell adds terminal-oriented conveniences such as flush/0 and dollar notation. Those are useful here, because the answers produced by a spawned toplevel arrive in the shell mailbox as ordinary messages.

Let's create a monitored toplevel and preload it with two facts:

?- toplevel_spawn(Pid, [
       session(true),
       monitor(true),
       load_list([p(a),p(b)])
   ]).

The most recent binding of Pid is the one used below.

Calling goals

Use toplevel_call/2-3 to make the spawned toplevel run a goal. With default options, the whole answer set is returned in one success(...) term.

?- toplevel_call($Pid, p(X)).

?- flush.

The shell should now have a message of the form success(Pid,[p(a),p(b)],false). The second argument carries the answers, and the third says whether more may exist.

Using the template option

If you only want the bindings, pass a template:

?- toplevel_call($Pid, p(X), [
       template(X)
   ]).

?- flush.

This time the success term should contain [a,b] rather than [p(a),p(b)].

Paging with limit, next, and stop

A toplevel remembers where it is in the search. Setting limit(1) gives a REPL-style one-answer-at-a-time interaction:

?- toplevel_call($Pid, p(X), [
       template(X),
       limit(1)
   ]).

?- flush.

?- toplevel_next($Pid).

?- flush.

On the first flush, the third argument of the success term should be true, meaning that the toplevel is waiting for next or stop. After toplevel_next/1, the remaining answer should come back.

The same mechanism works for larger result sets, including an infinite generator:

?- toplevel_call($Pid, between(1,infinite,I), [
       offset(100),
       template(I),
       limit(3)
   ]).

?- flush.

?- toplevel_next($Pid, [
       limit(5)
   ]).

?- flush.

?- toplevel_stop($Pid).

The second page request changes the page size on the fly. After that, toplevel_stop/1 drops the rest of the suspended search and returns the toplevel to idle.

Output, input, and reply

A toplevel can emit output while it is running a goal:

?- toplevel_call($Pid, output(hello)).

?- flush.

The shell should receive an output(Pid,hello) message, followed by the ordinary success term.

A toplevel can also prompt for input and wait for a reply. First ask it to call input/2:

?- toplevel_call($Pid, input('Input', X)),
   receive({Answer -> true}).

The shell should now receive a prompt term. Reply like this:

?- respond($Pid, hello),
   receive({Answer -> true}).

The returned success term should now contain input('Input',hello).

Aborting and halting

Because the toplevel is a separate actor, you can give it work that does not terminate, then abort that work without killing the session itself.

?- toplevel_call($Pid, assert((p :- p))),
   receive({Answer -> true}).

?- toplevel_call($Pid, p).

Nothing useful will appear in the shell, but the spawned toplevel is now spinning on its own. Abort the running goal like this:

?- toplevel_abort($Pid).

When you are done with the toplevel altogether, halt it:

?- toplevel_halt($Pid, Reply).

Remote calls with rpc/2-3

Web Prolog nodes can talk to each other. The predicate rpc/2-3 sends a goal to a remote node identified by a URI, receives the answers over the stateless /call API, and turns them back into ordinary Prolog backtracking.

Note that rpc/2-3 talks specifically to the remote node's stateless /call route. This remains true even if the target happens to be an ACTOR node. So rpc/2-3 can be used for remote logical queries, but not for actor-only goals such as self/1, spawn/1-3, or receive/1-2.

In this deployment, n1 hosts:

human(plato).
human(aristotle).

while n2 hosts:

mortal(Who) :- human(Who).

human(socrates).
human(Who) :-
    rpc('https://n1.elfenbenstornet.se', human(Who)).

From this portal, you can query n2 directly:

?- rpc('https://n2.elfenbenstornet.se', mortal(Who)).

You should get socrates from n2 itself, followed by plato and aristotle fetched remotely from n1.

The three-argument form of rpc/2-3 can also ship temporary source text with the call. Here mortal/1 is defined only for the duration of the remote query:

?- rpc('https://n3.elfenbenstornet.se', mortal(Who), [
       load_text("mortal(Who) :- human(Who).")
   ]).

The code passed in load_text/1 is not installed permanently; it exists only for that remote call.

Distributed proof trees

A defining advantage of logic-based AI is transparency: every conclusion rests on rules and facts, and an explanation can be constructed as a proof object. On the Prolog Web, explanations can survive distribution. If a subgoal is proved on another node, its justification is folded into the same overall proof structure rather than hidden inside an opaque remote call.

A compact meta-interpreter achieves this by treating remote calls as proof-producing sub-derivations. When the interpreter encounters a remote goal it ships itself to the remote node via load_predicates([prove/2]), so the remote node also runs prove/2 and returns a proof term. Remote contributions are marked in the result with the @ operator.

Note. This example only works on n3, which has the distributed mortal/1 and human/1 definitions in its shared database.

Load the interpreter into your session:

prove(true, true) :- !.
prove(rpc(URI, A), Proof) :- !,
    prove(rpc(URI, A, []), Proof).
prove(rpc(URI, A, Options), Query@URI/Proof) :- !,
    rpc(URI, prove(A, Query/Proof), [
        load_predicates([prove/2])
      | Options
    ]).
prove((A, B), (ProofA, ProofB)) :- !,
    prove(A, ProofA),
    prove(B, ProofB).
prove(A, A/Proof) :-
    clause(A, B),
    prove(B, Proof).

On n3, the shared database contains:

:- dynamic mortal/1, human/1.

mortal(X) :- human(X).

human(socrates).
human(X) :- rpc('https://n4.elfenbenstornet.se', human(X)).

while n4 hosts:

:- dynamic human/1.

human(plato).
human(aristotle).

Now ask for a proof:

?- rpc('https://n3.elfenbenstornet.se', prove(mortal(X), Proof), [
       load_predicates([prove/2])
   ]).

You should see something like:

Proof = mortal(socrates)/(human(socrates)/true),
X = socrates ;
Proof = mortal(plato)/(human(plato)/(human(plato)@'https://n4.elfenbenstornet.se'/true)),
X = plato ;
Proof = mortal(aristotle)/(human(aristotle)/(human(aristotle)@'https://n4.elfenbenstornet.se'/true)),
X = aristotle.

The first answer is proved entirely on n3. The second and third are proved by delegating to n4, and the annotation @'https://n4.elfenbenstornet.se' documents exactly where each remote step was resolved. The proof term is ordinary Prolog data: it can be inspected, stored, or rendered into a human-readable explanation.

Note. clause/2 can only inspect predicates declared dynamic.

The concurrent Prolog Web

Spawning, sending, monitoring, registering, and even toplevel_spawn/2 can work across node boundaries. In this deployment the two nodes n3 and n4 support remote actor operations. The examples below use the other node as the remote target.

Tip! These examples only make sense on n3 or n4. The remote URL shown below is rewritten automatically so it points at the other node.

Actor talking to remote actor

First spawn an echo_actor/0 on the other node, monitor it, and register it locally under a convenient name:

?- spawn(echo_actor, Pid, [
       node('{{actor_peer_host}}'),
       monitor(true),
       load_text("echo_actor :-
    receive({
        echo(From, Msg) ->
            From ! echo(Msg),
            echo_actor
    }).")
   ]),
   register(remote_echo_actor, Pid).

The pid is now a compound term of the form Id@'URL'. Send the remote actor a message and let it reply to your local shell:

?- self(Self),
   remote_echo_actor ! echo(Self, hello).

?- flush.

The actor lives on another node, but the reply still comes back to the shell you are talking to.

To finish the conversation, kill the remote actor and then receive the resulting down/3 message:

?- whereis(remote_echo_actor, Pid),
   exit(Pid, die).

?- receive({A -> true}).

The down/3 message is local, but it refers to the remote pid. That is the basic shape of network-transparent actor interaction.

Ping-pong across nodes

The same node(...) option can move the ponger to the other node. Load one helper predicate into your current session:

ping(0, Pong_Pid) :-
    Pong_Pid ! finished,
    format('Ping finished.~n', []).
ping(N, Pong_Pid) :-
    self(Self),
    Pong_Pid ! ping(Self),
    receive({pong -> format('Ping received pong.~n', [])}),
    N1 is N - 1,
    ping(N1, Pong_Pid).

pong :-
    receive({
        finished -> format('Pong finished.~n', []) ;
        ping(Ping_Pid) -> Ping_Pid ! pong, pong
    }).

ping_pong_remote :-
    spawn(pong, Pong_Pid, [
        node('{{actor_peer_host}}')
    ]),
    spawn(ping(3, Pong_Pid)).

?- ping_pong_remote.

You should only see the pinger's output rendered immediately in the terminal. That is expected: terminal output is only forwarded directly from actors on the local session lineage, not from actors running on the remote node.

The node option works for toplevels too

You can also spawn a toplevel on a remote node. The mechanism is the same:

?- toplevel_spawn(Pid, [
       node('{{actor_peer_host}}'),
       session(true)
   ]).

The remote nodes both host two simple human/1 facts, so we can ask the remote toplevel for one answer at a time:

?- toplevel_call($Pid, human(Who), [
       template(Who),
       limit(1)
   ]).

The answer comes back as a message to the local shell, so receive it:

?- receive({Msg -> true}).

?- toplevel_next($Pid).

?- receive({Msg -> true}).

The answers are produced on the remote node, but the protocol conversation happens through your local shell exactly as if the toplevel were nearby.

The server behaviour

The fridge simulation in the earlier section was a perfectly good actor, but it mixed three concerns into one receive loop: the message protocol, the state threading, and the application logic. The server behaviour factors the first two out into a reusable scaffold, leaving only the third for the application programmer. The scaffold also gives us synchronous request/reply, hot code swapping, and — when combined with a supervisor — automatic recovery from crashes.

A server is described by a single callback predicate of arity four:

Pred(+Request, +OldState, -Response, -NewState)

The same fridge, written in this style, becomes a small relation:

fridge(store(Food), List, ok, [Food|List]).
fridge(take(Food),  List, ok(Food), Rest) :-
    select(Food, List, Rest), !.
fridge(take(_Food), List, not_found, List).

Notice that the predicate has no idea it is going to be embedded in a process. It is just an ordinary Prolog relation between an old state and a new one. The same code can be tested on its own, reused outside the actor world, and reasoned about declaratively.

Starting a supervised server

Spawning a server through a supervisor takes a single call. The restart(permanent) option tells the supervisor to respawn the server (with the empty initial state) if it ever crashes:

?- supervisor_spawn([
       child(fridge, [
           start(server(fridge, [initial_state([])])),
           restart(permanent)
       ])
   ], Sup).

The server is registered under the name fridge, so clients can address it by name without holding on to its pid.

Synchronous calls

The client side of the protocol is hidden behind server_request/3. From the caller’s point of view it is just a remote procedure call:

?- server_request(fridge, store(milk), Response).

?- server_request(fridge, take(milk), Response).

Asynchronous calls

For applications that prefer not to block, the same call splits into a promise and a later yield. Between them the client is free to do other work:

?- server_promise(fridge, store(meat), Ref).

?- server_yield($Ref, Response).

Fail-fast and recovery

What happens if a client sends a malformed request? Our fridge/4 has no clause for sore/1, so the server crashes:

?- server_request(fridge, sore(milk), Response).

The shell reports Unknown message: server_down(false) rather than blocking forever, because server_request/3 installs a monitor under the hood. Behind the scenes, the supervisor has already respawned the server under the same registered name, with its state reset to the initial empty list. The next call goes through to the freshly born server:

?- server_request(fridge, store(eggs), Response).

Hot code swap

A crashing server is not a long-term plan. The proper fix is a defensive callback that returns an error response instead of crashing. Here is one:

fridge2(store(Food), List, ok, [Food|List]).
fridge2(take(Food),  List, ok(Food), Rest) :-
    select(Food, List, Rest), !.
fridge2(take(_Food), List, not_found, List).
fridge2(_Other,      List, error(unknown_request), List).

We can swap the running server’s callback in place with server_upgrade/2. The pid does not change, the registered name does not change, and crucially the current state is preserved:

?- server_upgrade(fridge, fridge2).

The same bad request that previously killed the server now returns a polite error:

?- server_request(fridge, sore(milk), Response).

And valid requests continue to thread through the state we accumulated before the upgrade:

?- server_request(fridge, store(butter), Response).

Tearing down

A graceful stop sends a final message and waits for the acknowledgement:

?- supervisor_halt($Sup).

What we have just walked through is a complete Erlang-style behaviour pattern in miniature. The fixed scaffolding — message protocol, state threading, monitoring, supervision, hot upgrade — lives in the library; the application logic is a four-argument relation that a Prolog programmer can write, test, and reason about as ordinary declarative code.

Statechart actors

Statechart actors bring the full power of hierarchical state machines to Web Prolog actors. A statechart is described in a small XML dialect and interpreted at run time: spawning a statechart actor produces an ordinary actor pid, and the statechart reacts to events — ordinary Web Prolog messages — by moving between states, running entry and exit actions, and delegating to history states so that a machine can resume exactly where it left off.

Pause and resume

A classic problem in reactive systems is pause and resume: suspending a running process while preserving its internal state, and later continuing from the same point. The statechart below models a small game with two substates (s1 and s2) that can be paused at any moment and resumed from the exact substate in which they were interrupted.

The diagram shows the structure. The outer game state has two children: play (the running game, itself containing s1 and s2) and interrupted. The history node H inside play records which substate was active at the moment of a pause, so that a resume transition can return to it directly.

game play interrupted s1 s2 H play reset pause resume stop

Statechart for a pausable game. The H node is a history state: resuming transitions to the last active substate of play rather than to its declared initial state. The diagram updates live as transitions fire.

Spawn the statechart as an actor. load_uri fetches the XML for the interpreter, and trace(true) enables transition tracing so the diagram updates automatically as events fire. Pid is bound to the actor's pid. Spawning also populates the Examples menu (the button below the terminal input) with all five event queries:

?- statechart_spawn(Pid, [
       load_uri('/examples/statecharts/01%20pause-and-resume.xml'),
       trace(true)
   ]).

The machine starts in s1. Use the Examples menu to send events one at a time and watch the diagram respond:

$Pid ! play advances s1s2
$Pid ! reset steps back s2s1
$Pid ! pause saves current substate in H, enters interrupted
$Pid ! resume restores H — returns to the substate active at pause time
$Pid ! stop drives the machine to final and terminates the actor
Tip! Enable the SXML trace filter in the Logger to watch every state entry, exit, and transition in real time.

The XML source is reproduced here for reference:

<statechart datamodel="web-prolog" initial="game">
    <state id="game" initial="play">
        <state id="play">
            <initial>
                <go to="s1" />
            </initial>
            <history id="h">
                <go to="s1" />
            </history>
            <state id="s1">
                <go to="s2" on="play" />
            </state>
            <state id="s2">
                <go to="s1" on="reset" />
            </state>
            <go to="interrupted" on="pause" />
        </state>
        <state id="interrupted">
            <go to="h" on="resume" />
        </state>
        <go to="final" on="stop" />
    </state>
    <final id="final" />
</statechart>

Reading a spaghetti statechart

Real statecharts can look tangled at first. The visual trick is to read them from the outside in: identify the active leaf state, then walk outward through its parent states until you find a transition whose event matches the message you send. The first enabled transition determines the next state.

Spaghetti statechart with nested states s0, s1, s11, s2, s21, and s211

Spaghetti statechart.

The machine starts in s0, follows its initial transition into s1, and then follows s1's initial transition into s11. If you send c while the active leaf is s11, there is no c transition on s11, so the interpreter looks at the parent s1 and takes s1s2. Entering s2 then follows nested initial transitions down to s211.

Spawn the chart and keep the Logger open with the Statechart Trace filter selected. The trace shows the same process as text: the current configuration, the external event, exits, transitions, and entries.

?- statechart_spawn(Pid, [
       load_uri('/examples/statecharts/02%20spaghetti.xml'),
       trace(true)
   ]).

Use the Examples menu to send events one at a time:

$Pid ! a loops on s1 and re-enters its initial leaf s11
$Pid ! b targets s11 from s1, or s211 from s21
$Pid ! c moves between the two large sibling regions s1 and s2
$Pid ! d resets outward to s0, or from s211 to s21
$Pid ! e uses the outer s0 transition to enter s211
$Pid ! f crosses between s1, s11, and s211
$Pid ! g shows leaf-level precedence from s11 or s211
$Pid ! h takes s211 to final state f and terminates the actor

NPC emotions

Parallel states let one actor be in several independent states at the same time. This toy non-player character has two emotion dimensions and one behavior mode. At any moment it has one active state in the anger/fear dimension, one in the anticipation/surprise dimension, and one in behavior.

NPC emotion statechart with parallel emotion and behavior regions

Orthogonal regions: two emotion dimensions plus one behavior mode.

The point of orthogonality is that related but independent dimensions do not have to be flattened into one large state name. Without parallel regions, combinations such as anger_anticipation_attacking and fear_surprise_fleeing would have to be encoded as separate states, and the number of combinations would grow quickly.

Here, event e can be read as an explosion. Because all three regions are active, the same event is observed in each region: anger becomes fear, anticipation becomes surprise, and attacking becomes fleeing in one coordinated step.

?- statechart_spawn(Pid, [
       load_uri('/examples/statecharts/03%20emotions.xml'),
       trace(true)
   ]).

Use the Examples menu to send the three events:

$Pid ! e moves to fear, surprise, and fleeing
$Pid ! d moves fear back to anger and fleeing back to attacking
$Pid ! f moves surprise back to anticipation

Computing the GCD

The time is ripe to introduce Web Prolog as a scripting language inside statecharts. Executable Web Prolog code in transition bodies and guards increases their power significantly: a chart can update data, run relations, produce output, and still keep the surrounding control logic explicit.

This statechart applies Euclid's algorithm to find the greatest common divisor of a list of integers. The machine has two states: init waits for a single input/1 event carrying the list, then run iterates — repeatedly replacing the larger of any two values with their difference — until only one value remains and the result is printed.

<statechart datamodel="web-prolog" initial="init">
    <datamodel>
        :- dynamic int/1.
    </datamodel>
    <state id="init">
        <go on="input(List)" to="run">
            forall(member(X, List), asserta(int(X)))
        </go>
    </state>
    <state id="run">
        <go if="int(X), int(Y), X > Y">
            Z is X-Y,
            retract(int(X)),
            assert(int(Z))
        </go>
        <go to="stop" if="int(X)">
            writeln(X)
        </go>
    </state>
    <final id="stop" />
</statechart>

Spawn the machine. The Examples menu will be loaded with a sample query:

?- statechart_spawn(Pid, [
       load_uri('/examples/statecharts/08%20gcd.xml'),
       trace(true)
   ]).

Now send the input list:

?- $Pid ! input([25, 10, 15, 30]).

The GCD 5 is computed and written to the terminal, and the machine has moved to its final state.

Process invocation and communication

Statecharts become more useful once they can spawn other processes and communicate with them. In Web Prolog, the invoked processes are simply actors: toplevels, other statechart actors, or services running on local or remote nodes.

This final example spawns a child toplevel, submits a query to it, prints each answer as it arrives, and terminates when the last answer has been received. The spawned toplevel is initialized with a small program: q(X) :- p(X) and four facts, p(a) through p(d).

?- statechart_spawn(Pid, [
       load_uri('/examples/statecharts/10%20spawn-toplevel.xml'),
       trace(true)
   ]).

When the surrounding state becomes active, the <spawn> element creates the child toplevel. The generated spawned(Pid) event reports the pid of that child actor, and the transition from ask to collect calls toplevel_call/3 with limit(1).

<statechart datamodel="web-prolog" initial="spawn-ask-collect">
    <state id="spawn-ask-collect" initial="ask">
        <spawn type="toplevel" exit="false">
            q(X) :- p(X).
            p(a). p(b). p(c). p(d).
        </spawn>
        <state id="ask">
            <go to="collect" on="spawned(Pid)">
                toplevel_call(Pid, q(X), [
                    limit(1)
                ])
            </go>
        </state>
        <state id="collect">
            <go to="collect" on="success(Pid, Data, true)">
                writeln(Data),
                toplevel_next(Pid)
            </go>
            <go to="final" on="success(_, Data, false)">
                writeln(Data)
            </go>
        </state>
    </state>
    <final id="final" />
</statechart>

Each answer arrives as success(Pid, Data, More). While More is true, the chart prints the current answer and asks the child toplevel for the next one with toplevel_next/1. When More is false, the last answer has arrived and the machine enters its final state.

Variables bound by the on pattern, and by any if guard, are in scope in the executable content of the same <go> element. The intent of <spawn> is also ownership: the helper process belongs to the control state that created it, so leaving that state can clean up the invoked actor without ad hoc shutdown code.

More statechart examples

The examples above are just a starting point. Many more statecharts are available in the Examples drawer on the left side of the workbench — click the arrow to open it and select any file from the Statecharts section. Loading a file opens it in the editor and populates the terminal’s Examples menu with its built-in queries, ready to run.

Exercising the web APIs

A Web Prolog node currently exposes three web-facing APIs. The stateless HTTP API uses /call for self-contained query requests. The semi-stateful HTTP API uses the /toplevel_* endpoints to keep a toplevel session alive across several HTTP requests. The stateful WebSocket API uses /ws for fully interactive, message-based browser sessions.

The stateless HTTP API

A Web Prolog node can also be queried directly through the stateless /call endpoint. Because the API is stateless, each request stands on its own. That makes it easy to try from the browser address field.

This link runs append([a],[b,c],Xs) and returns JSON:

{{host}}/call?goal=append([a],[b,c],Xs)&format=json

These two links page through the answers to human(Who) on n4 one slice at a time:

https://n4.elfenbenstornet.se/call?goal=human(Who)&limit=1&offset=0

https://n4.elfenbenstornet.se/call?goal=human(Who)&limit=1&offset=1

In other words, a Web Prolog node can be used both as an interactive Prolog portal and as a simple web API for Prolog queries.

The semi-stateful HTTP API

When one request is not enough, the node also exposes the /toplevel_spawn, /toplevel_call, and /toplevel_next endpoints. Together they provide a browser-side session with paging, output, prompts, and explicit follow-up requests.

Two runnable pages demonstrate the current HTTP session protocol:

  • HTTP demo 1 spawns a toplevel, calls member(X,[a,b,c]) with limit=1, and fetches the remaining answers one slice at a time.
  • HTTP demo 2 loads a session-local program, handles prompt events, replies with toplevel_respond, and continues with toplevel_poll.

The stateful WebSocket API

For fully stateful browser-side interaction, use /ws. The commands understood by the current node are toplevel_spawn, toplevel_call, toplevel_next, toplevel_respond, spawn, send, and exit.

The following pages are small runnable demonstrations of the current protocol:

  • WebSocket demo 1 queries a toplevel and pages through the answers with toplevel_next.
  • WebSocket demo 2 spawns a browser-owned actor and exchanges messages with the node-resident pubsub_service.