A book of ideas and proposals for the future of Prolog

Hi all,

This is my first message in this forum, and since I am not a central figure in the Prolog community, I should probably begin with a brief introduction.

My name is Torbjörn Lager. I am professor emeritus of general and computational linguistics at the University of Gothenburg. I have worked with computational linguistics, dialogue systems, web technologies, and related matters for many years. I have also had a long-standing interest in Prolog, although I have always been somewhat of an outsider to the main Prolog and logic programming communities. (Unfortunately, Chalmers here in Gothenburg is a FP stronghold, with little interest in LP.)

For almost fifteen years, on and off, I have been working on a book-length project called The Prolog Trinity ecosystem. The project is perhaps best described as a book of ideas, but it is not only that. It also comes with a fairly extensive online demonstrator and links to GitHub repositories containing prototype implementations. The current draft is about 350 pages long and introduces a proposal for three closely related things: Web Prolog, Prolog agents, and the Prolog Web.

In the book, these three together form what I call the Prolog Trinity ecosystem: Web Prolog as a profile of Prolog for web-based interaction, Prolog agents as logic-programmable actors, and the Prolog Web as a possible distributed environment in which such agents and nodes can interact.

The central technical idea is that Prolog could be given a web-facing profile that combines ordinary Prolog-style querying with Erlang-inspired concurrency and distribution. In such a setting, different Prolog systems could potentially communicate over the Web using a common interaction model, without requiring one Prolog system to become the Prolog system.

So one part of the proposal is a concurrent interface, inspired by Erlang, where Prolog actors have mailboxes, process identifiers, private clause databases, and communicate by message passing. These actors may live on different nodes, and the intention is that local and remote interaction should be made as uniform as is reasonably possible.

Another part is a sequential interface, where a node can query another node through something like rpc/2-3, with ordinary Prolog backtracking exposed across HTTP. In this view, backtracking can be understood as a form of pagination: a remote query may produce a stream of answers, but these answers can be requested incrementally by the client.

A third part is the broader architectural idea: a Prolog Web consisting of Prolog nodes that expose logic-driven services, can be queried by browsers and other web clients, and may host longer-lived Prolog agents. The idea is not that the Prolog Web should replace the existing Web, or the Semantic Web, but that it could become a small logic-oriented layer within the wider Web ecosystem.

To some extent, the project can also be read as a reaction to the discussions around the future of Prolog, including the 2022 article Fifty Years of Prolog and Beyond. I found that article valuable, but I also felt that Prolog needs more explicit long-range visions: not only better libraries, better tooling, and better portability, but also bolder architectural stories about what Prolog might become in a web-native, agent-oriented, AI-saturated world.

During the last five years, after retiring from my position at the University of Gothenburg, the project has grown from a rather speculative manuscript into something more concrete. For a long time I did not think I would be able to finish anything worth showing to others, but thanks in part to LLMs — mostly Claude, but also ChatGPT — I now believe I have something reasonably presentable.

The project homepage is here:

https://trinity.elfenbenstornet.se/

From there, one can find the book draft, a fairly comprehensive online demonstrator, and links to the relevant repositories.

I would be very grateful for thoughts, comments, criticism, or suggestions — especially from the perspective of Prolog implementers and experienced Prolog programmers.

I do not present this as finished work, and certainly not as the future of Prolog. It is a proposal, a prototype, and a provocation. But I hope it may be interesting to people who care about Prolog’s future — especially those who think that Prolog still has something important to contribute to the Web, to symbolic AI, and to the next generation of intelligent systems.

Best regards,
Torbjörn

2 Likes

Just to give my post about the Trinity a bump, I have written a blog post that I believe is suitable for a forum with system-independent discussions of all things Prolog.

This is a very impressive combination and implementation of Prolog with those other technologies. Would you like to have a conversation with our Prolog Education Group? We could record your presentation to be shared at: https://www.youtube.com/@LogicProgrammingEducation

Hi! Sure, I’d love to do that! So let’s agree about a date and time. And, if you can, let me know what aspects of my work you would be most interested in.

1 Like

Please, check your direct messages. Thank you

Hello,

Really interesting topic, also would be good to add into book new prolog ideas like http://tln-lang.org/

I have some doubt that Web Prolog captures the wisdom
of 70 years of distributed database engineering. Or maybe
the Trinity Web Prolog concept paper does not talk about it.

But the general structure of SQL queries is:

SELECT projection
FROM source
WHERE condition

Now I have a problem with Web Prolog when I would like
to do very simple remote things like for example, which can be
highly optimized in traditional WAM based Prolog systems,

the way anonymous variables can be represented. You see
this in the GNU Prolog FFI, in the C declarations from <gprolog.h>,
where an argument can be marked as wanted on return or not:

?- ..., PID ! person(ID, _, _, AGE, _), ...

What a Web Prolog might see at runtime is only:

?- ..., 9167 ! person(3524, _1, _2, _3, _4), ...

The pengine, although it can do paging or whatever, will
then happily send back over the wire a very large JSON
packet which will have the answer in its data field:

{
   ...
   data:{"_1":..., "_2":...", "_3":..., "_4":...}
}

But from the intention of the original query we would like only:

{
   ...
   data:{"_3":...}
}

So while Web Prolog gives me some traits of a ResultSet
from SQL, like paging. I think a mechanism for select projection
is missing, while SQL happily does that as well in its ResultSet.

Its quite a tricky problem, since it shatters the whole idea of
a simple (!)/2 binary operator for Prolog based RPC. Maybe it
can be solved on the compiler side with an analysis that

provides the GNU Prolog FFI flags. But then the Prolog based
RPC becomes more than a new built-in with a beautifully
looking (!)/2 binary operator, it would also require some

intervention in compiler technique from the Prolog system.

Bye

P.S.: A variant 1 workaround would be to allow quantifiers in
a query, like for example (^)/2 as already found in setof/3.
But then one runs into the annoying opt-out accumulation:

?- ..., PID ! X1^X2^X3^person(ID, X1, X2, AGE, X3), ...

P.P.S.: Although switching from opt-out to opt-in would
be a solution in this matter, sending programs could be also
a variant 2 solution, for example an RPC where I can send a temporary

program and a query would also do the job, see this rpc/3 example:

?- ..., rpc(PID, person_age(PID, AGE), 
         {person_age(PID, AGE) :- person(ID, _, _, AGE, _)}), ... .

It has mindboggling variable scoping, {}/1 doing automatic
universal quantification, just like input clauses are read in a
Prolog text according to ISO core standard semantics, but

practically one runs into problems whether the provided program
should more have some embedded implication semantics and have
not all variables automatically universally quantified, so as to capture

terms, and here transmit to the PID, from the calling context.

Hi,

In Web Prolog, Pid ! Message is asynchronous actor message sending, and is not relevant to projection. It does not invoke an actor and does not return a substitution. Variables in the message are copied as fresh variables at the receiving actor.

Remote queries instead use toplevel_call/2-3 or the sequential rpc/2-3 interface. At the toplevel protocol level, projection is already available through the template/1 option:

toplevel_call(Pid, person(3524, _, _, Age, _), [template(Age)]).

The reply then contains instantiations of Age, rather than instantiations of the entire person/5 goal. The stateless /call protocol has the corresponding template mechanism. Thus the protocol does have an analogue of SQL’s SELECT projection.

The recommended way to do projection in a distributed setting in Web Prolog is rpc/2-3. If you go to The Prolog Trinity demonstrator you can do this:

?- rpc('https://n2.elfenbenstornet.se', father_child(X,_)).
X = tom ;
X = tom ;
X = mike.
?- 

or

?- rpc('https://n2.elfenbenstornet.se', father(X), [
       src_list([(father(X) :- father_child(X,_))])       
   ]).
X = tom ;
X = tom ;
X = mike.
?- 

I recommend taking the ACTOR profile tutorial at The Prolog Trinity demonstrator , and to play around with the example programs. I really appreciate your questions, but it would be much easier for you to ask them, and for me to answer them, if we use the demonstrator.

The Web Prolog Manual should answer many of the questions that you may have.

But the idea of projections is hardly used. Also throughout the
coding examples. Take the parallel/1 demonstration, which
reads as follows on the Web Prolog Trinity demo examples site:

myparallel(Goals) :-
    maplist(par_solve, Goals, Pids),
    maplist(par_yield(Pids), Pids, Goals).

Judging from the implementation of par_yield, each goal is used as
the projection template for itself. But one could at least use
term_variabels/2, which is from ISO Prolog, on the goals to realize:

myparallel(Goals) :-
    maplist(par_solve, Goals, Pids, Projs),
    maplist(par_yield(Pids), Pids, Projs).

With these helper predicate realizations:

par_solve(Goal, Pid, Proj) :-
    term_variables(Goal, Proj),
    self(Self),
    spawn((call(Goal), Self ! Pid-Proj), Pid, [
        monitor(true)
    ]).

par_yield(Pids, Pid, Proj) :-
    receive({
        Pid-Proj->
            receive({
                down(Pid, _, true) ->
                    true
            }) ;
        down(_, _, false) ->
            tidy_up_all(Pids),
            fail ;
        down(_, _, exception(E)) ->
            tidy_up_all(Pids),
            throw(E)
    }).

With the above modification, a goal would at least not anymore send a
big input argument back to the parallel/1 call site. The receive has no
idea what to project and what to not project. So the send (!)/2 has

already to do the projection. And we compute the projection template
ahead of time via the built-in term_variables/2. You find this use of
term_variables/2 in SWI-Prolog predicates such as distinct/1 as well.

It is a kind of runtime cludge, a compiler intervention could be more
efficient doing it once instead for every invocation. The runtime versus
compiled discrepancy is sometimes solved in the macros library of

SWI-Prolog, like when lambda expressions are compiled ahead of
time. This is a nice example where auxiliary predicates are created
so that costly runtime calling of complex goals is not needed at all.

Such macro tricks are admitedly more difficult for a Web Prolog, but
not impossible, but I havent seen something at all yet.

I agree that term_variables/2 can be very useful, but I don’t think it makes sense here. I get the impression that you think that parallel/1 is doing distribution, but it’s only doing local concurrency. (It can be compared with SWI-Prolog’s concurrent/3, but thanks to the more high-level Erlang-style predicates – high-level if compared to the thread_* predicates – it can be implemented more succinctly with (almost) the same functionality.)

See section 5.5 in the book draft for a more correct implementation of parallel/1.

See section 5.4.3 for how term_variables/2 is used in the implementation of rpc/2-3. That’s where it absolutely makes sense as it can significantly reduce what is being sent over an HTTP connection. (It would make sense for the implementation in section 5.4.2 too, but I wanted to keep that code as small as possible.)

Yes, projecting the variables could reduce local message-copying when goals contain substantial ground data. My point was that parallel/1 involves no network transfer, while rpc/2-3 already performs this projection where it matters most. Since parallel/1 returns only one solution per goal, the claimed N-answer amplification does not arise there (while it does arise in the case of rpc/2-3). Whether the local change is worthwhile is therefore an implementation question that should be benchmarked.

And no, I do not think every tutorial example should incorporate every potentially useful optimisation. The purpose of this parallel/1 example is to make the actor coordination and the propagation of bindings easy to see. Sending the instantiated goal back makes that mechanism especially direct. Introducing a separate projection at this point would add machinery that is not central to the example.

Nope doesn’t show message send and receive. I was mistaken.
Can one use trace/0 inside your web browser demo? Will tracing
be inherited by child actors? When the parent actor was traced?

How does this semantic emerge, without once/1 in par_solve:

parallel([G1,..,Gn])   <==> (once(G1); ..; once(Gn))

Whats the tracing suggestion here, to see whats going on?

You are right that the Logger does not show ordinary actor sends and receives.

First, I assume the semicolons in your proposed equivalence are a typo. For independent goals the intended correspondence is a conjunction on that right hand side rather than disjunction:

parallel([G1, ..., Gn]) <==> (once(G1), ..., once(Gn))

The explicit once/1 is unnecessary because the goal passed to spawn/2-3 becomes the actor’s top-level goal. When

call(Goal), Self ! Pid-Goal

succeeds, that top-level goal has completed and the actor terminates successfully. Nothing asks the actor to backtrack for another solution, so its remaining choice points are discarded. Adding once/1 make this explicit, but doesn’t change the behaviour.

The demonstrator doesn’t do tracing (yet). It may be possible to trace this using an ordinary local SWI-Prolog installation. It should be enough to load actors.pl together with the implementation of parallel/1.

Ok, I see. I thought it will be automatically redone, and thus
rapid fire multiple solutions. So you have already hardwired a
once/1 semantic into your spawn/3, in that the spawn/3 goal

argument when executed, is onced, only looking at its EXIT
and FAIL port, not calling further times its REDO port. This
also means that both parallel/1 and first_solution/1 have a

“short-cut” AND respective “short-cut” OR semantic:

/* AND-parallelism with short-cut */
parallel([G1,..,Gn])   <=>   once(G1), .., once(Gn)

/* OR-parallelism with short-cut */
first_solution([G1,..,Gn])   <=>   once(G1); ..; once(Gn)

The “short-cut” is in that parallel/1 can stop at the first failure,
and that first_solution/1 can stop at the first success. I wonder
whether Web Prolog Trinity offers true OR-parallelism and/or

true AND-parallelism as well without a such “short-cut”?

Yes, the once-like description of an actor’s top-level goal is essentially correct. Two small notational qualifications: the last goal should be Gn, and the Web Prolog predicate is first_solution/2-3, with a separate solution template. It is also a concurrent race rather than an ordinary left-to-right disjunction.

For context, these contracts are closely modelled on Jan Wielemaker’s concurrent/3 and first_solution/3 in SWI-Prolog’s library(thread). My aim was to reproduce similar behaviour using Web Prolog actors. I chose the name parallel/1 because concurrency has many other purposes in Web Prolog – application structure, fault resistance and distribution – not merely making computations faster.

Web Prolog currently provides no general, noncommitted AND- or OR-parallel search operators. Its toplevel and RPC protocols can stream multiple solutions, so restricted variants could be constructed. But a transparent general implementation would amount to building a parallel Prolog search engine over isolated actors, which is outside the present scope of the Trinity.

See the following references for some of the research behind those predicates (mail me if you have trouble accessing these papers):

@inproceedings{pmoura09a,
author = {Paulo Moura and Ricardo Rocha and Sara C. Madeira},
title = “{High Level Thread-Based Competitive Or-Parallelism in Logtalk}”,
booktitle = {Proceedings of the Eleventh International Symposium on Practical Aspects of Declarative Languages},
editor = “Andy Gill and Terrance Swift”,
series = “Lecture Notes in Computer Science”,
volume = “5418”,
month = jan,
year = {2009},
pages = {107–121},
publisher = “Springer-Verlag”,
address = “Berlin Heidelberg”
}

@inproceedings{pmoura08b,
author = {Paulo Moura and Ricardo Rocha and Sara C. Madeira},
title = “{Thread-Based Competitive Or-Parallelism}”,
booktitle = {Proceedings of the 24th International Conference on Logic Programming},
series = “Lecture Notes in Computer Science”,
editor = “Maria Garcia de la Banda and Enrico Pontelli”,
volume = 5366,
month = dec,
year = 2008,
pages = {713–717},
publisher = “Springer-Verlag”,
address = “Berlin Heidelberg”
}

@inproceedings{pmoura08a,
author = {Paulo Moura and Paul Crocker and Paulo Nunes},
title = “{High-Level Multi-threading Programming in Logtalk}”,
booktitle = {Proceedings of the Tenth International Symposium on Practical Aspects of Declarative Languages},
editor = “Paul Hudak and David S. Harren”,
series = “Lecture Notes in Computer Science”,
volume = “4902”,
month = jan,
year = {2008},
pages = {265–281},
publisher = “Springer-Verlag”,
address = “Berlin Heidelberg”
}

1 Like

Perhaps it is time to wrap up the discussion of parallel/1 and first_solution/2-3?

First, your insistence on using term_variables/2 has had an effect: I am going to use it in the demonstrator’s implementation of parallel/1. I also noticed that Jan Wielemaker uses the same technique in his implementation of concurrent/3, so its value in this context now makes more sense to me. Thank you.

I still intend to retain the instantiated-goal version in the pedagogical examples, however, because it makes the propagation of bindings particularly easy to see.

Incidentally, the noncommitted AND-parallelism you asked about seems closer to SWI-Prolog’s concurrent_and/2-3, which library(thread) explicitly credits to you. I have not attempted to reproduce that predicate in Web Prolog; doing so would be a separate and substantially more ambitious piece of work.

Now let me use this observation of yours as a transition to another important kind of actor in Web Prolog:

Ok, I see. I thought it will be automatically redone, and thus rapid fire multiple solutions.

I take it that we now agree that this is not how parallel/1 works.

Nevertheless, the once-like semantics at the actor boundary does not prevent an actor from enumerating a nondeterministic goal internally. For example, a worker could use a failure-driven loop:

enumerate(Self, Pid, Template, Goal) :-
    (   call(Goal),
        Self ! solution(Pid, Template),
        fail
    ;   Self ! done(Pid)
    ).

Such a worker sends every solution before reporting completion. This leads naturally to the toplevel actor, which implements a considerably more developed version of the same general idea: it turns a nondeterministic Prolog computation into a protocol-level stream of answers.

The book discusses toplevel actors in several places:

  • Section 3.3 introduces the concept and provides examples of its use.
  • Section 4.1.3 illustrates the spawning of a remote toplevel actor.
  • Section 5.3 gives a fairly detailed implementation sketch.
  • Section B.3 of Appendix B provides the manual entries for the toplevel_* predicates.

The demonstrator’s ACTOR-profile tutorial also contains several examples:

I regard the toplevel actor as perhaps the most important building block in the Trinity architecture. It contains the core of a Prolog shell and also plays an important role in one of the server-side caching strategies used behind the stateless HTTP API.

A toplevel is a first-class actor: many toplevels may run concurrently, each with its own process identity, mailbox, query state and lifecycle. Each can deliver a stream of solutions – and associated output – to its parent or another designated target. Streams from two or more toplevel actors can then be multiplexed or merged explicitly by a coordinating actor.

Hi,

Well its not a OR-parallelism, but AND-parallelism. The OR-parallelism
woud be another predicate. But the two are related, you can simulate
OR-parallelism by doing:

(A | B) :- concurrent_and(
                 between(1,2,Branch), 
                 (Branch=1 -> A; 
                  Branch=2 -> B)).

The predicate concurrent_and/2 also supports surrounding cut. At least
my implementation which was called balance/1 could do it. Don’t know
whether it survived into SWI-Prolog. But in as far you have:

first_solution([A,B])    <=>   once((A | B)).

Then your enumerate predicate has only one goal argument. Its probably
more a findall/3 and not some parallel OR or AND, that two or more
goal arguments. I don’t understand your usage of the word “toplevel”.

“toplevel” usually refers to the Prolog REPL. Prolog has already
a REPL, why do I need to call another REPL? I guess the problem
is that libedit of Prolog or the web interface, doesn’t allow actors

running in parallel and using a shared output stream?

Bye

Yes, concurrent_and/2-3 implements AND-parallelism. That is why I described it as noncommitted AND-parallelism.

Your use of between/3 as the generator is nevertheless an interesting way of using concurrent_and/2 to distribute alternative branches. And yes, the handling of a surrounding commitment survived into SWI-Prolog: its documentation says that if the caller commits after an answer, outstanding resources are reclaimed.

I do not, however, think that

first_solution([A,B]) <=> once((A ; B))

is an operational equivalence. once((A ; B)) gives priority to A: it tries B only according to ordinary sequential Prolog control. first_solution, by contrast, starts both computations and returns the result of whichever finishes first, cancelling the loser. If A is slow or does not terminate while B succeeds quickly, the difference is particularly clear. They share committed, single-result behaviour, but not the same search strategy or necessarily the same answer.

The enumerate/4 example was not intended as an implementation of AND- or OR-parallelism. It illustrated a different point: once-like behaviour at an actor boundary does not stop an actor from enumerating a nondeterministic goal internally and communicating its answers.

Nor is that quite the same as findall/3. findall/3 collects all answers and returns a completed list. The example sends answers incrementally, so another actor can begin consuming them before the search has finished, cancel the computation, redirect its output, or process an unbounded stream.

I use the term toplevel actor quite deliberately. A conventional Prolog shell normally bundles together a user interface – the prompt, terminal and answer display – with the machinery that executes queries and obtains further solutions.

Trinity separates these roles. The shell’s query-processing component is itself a toplevel actor governed by PTCP. The terminal and controller submit goals to it, request further solutions, display its answers and output, and allow the current computation to be stopped or aborted.

But a toplevel actor need not be attached to an interactive shell at all. It can be spawned programmatically, run locally or remotely, and send its answers and output to another actor. Conversely, an interactive Prolog shell can be understood as a user interface attached to one such actor.

Thus “toplevel” retains its familiar Prolog meaning, while “actor” indicates that the query-processing machinery has been made independently addressable, concurrently executable and controllable through messages. SWI-Prolog likewise speaks of opening “a new toplevel that runs concurrently” with the initial toplevel.

This idea is also not specific to WASM. A toplevel actor may be local or remote, and client code interacts with it through the same protocol.

I wrote the | ( parallel or ) and not the ; ( sequential or ):

P.S.: In a multi-threading Prolog system its trivial to share
the output streams with actors. You just need to make the stream
thread save. In a cooperative multitasking Prolog the approach

would be that reading from input stream suspends, and gives way
so that actors can cooperatively run. Python and JavaScript
node toplevels do that, they allow tasks to run, still they are

single threaded! You see that here, I ported the Python and
JavaScript behaviour to Java. And I am experimenting with
a predicate current_task/1 that retrieves the alive tasks:

I must admit its a little bit confusing. Unix shell jobs are better.