Threaded queries? Rulebase independence?

Sorry. Late … Indeed, this doesn’t work. I think Logtalk has extends for that. In plain SWI-Prolog you can get this to work, but the rules look a bit ugly. Sure you can write term expansion rules to deal with that though.

edit (this is not the only way)

rules.pl

:- module(rules,
          [ rule/1
          ]).
:- meta_predicate
    rule(:).

rule(M:X) :- M:fact(X).

fact1.pl

:- module(fact1,
          []).
:- use_module(rules).

fact(sure).

Running …

swipl rules.pl fact1.pl
?- 1 ?- fact1:rule(X).
X = sure.

Why the word “broken”? My understanding of any language featuring a garbage collector is that the memory used by what’s collected will eventually be reclaimed, not that memory is recovered on the spot. As long as, in this case, predicate clauses don’t come back haunting the user visible code, all should work fine. That have always been my experience with SWI-Prolog garbage collector (which is one of the best among Prolog systems). A simple demonstrative example in my case would be:

?- create_object(foo, [], [public(a/1)], [a(1),a(2),a(3)]).
true.

?- foo::a(N).
N = 1 ;
N = 2 ;
N = 3.

?- abolish_object(foo).
true.

?- current_object(foo).
false.

?- foo::a(N).
!     Existence error: object foo does not exist
...

?- create_object(foo, [], [public(a/1)], [a(4),a(5),a(6)]).
true.

?- foo::a(N).
N = 4 ;
N = 5 ;
N = 6.

Thus, all working as expected. Same when dealing with dynamic clauses in static objects.

@stuz5000 Is the “many worlds” design pattern described at https://logtalk.org/2019/11/13/many-worlds-design-pattern.html a good fit for your problem? I only had time to quickly skim over the details you posted. I will try to do that later.

@pmoura Yes. I does look like a good fit. I like the features that logtalk offers. However, I’m a little apprehensive to use it. Its like asking a C programming to use C++ – it makes more sense to bite off as complexity grows. (In my imagination only) I expect logtalk is a leaky abstraction the same way as C++ is - every strange problem can be self-solved IFF you understand how the abstraction works (vanilla Prolog has this issue too, and results in a steep learning curve). I’m relearning Prolog (after a 25 year hiatus). Baby steps for me.

My suggestion is for you to take a look to short tutorial and see if you’re comfortable with the basics described there.

1 Like

This is an interesting use (or abuse, depending on the perspective :smile:) of the meta_predicate/1 directive. The main issue of your solution is scalability, however. For example, the following will not work:

:- module(rules, [rule/1]).

:- meta_predicate(rule(:)).

rule(X) :-
	transform1(X).

transform1(X) :- 
	Y is X * 2,
	transform2(Y).

transform2(M:X) :-
	Y is abs(X),
	M:fact(Y).

We will get:

?- fact1:rule(1).
ERROR: Arithmetic: `fact1/0' is not a function
ERROR: In:
ERROR:   [12] _6690 is (fact1:1)*2
...

I.e. is not enough to make all exported predicates that may, directly or indirectly, calling fact/1 meta-predicates (thus forcing you to track these dependencies). You also need to make all intermediate predicates (transform1/1 in my example) aware that one or more arguments are (or may be!) module qualified. In this example, being forced to write instead:

transform1(M:X) :- 
	Y is X * 2,
	transform2(M:Y).

With this change, we do get:

?- fact1:rule(1).
true.

With a set of non-trivial rules, each with multiple arguments, you risk ending up with module prefixing in most predicates. Notably, as I exemplified, in intermediate predicates. Elegance of the minimal example you provided is thus quickly lost and you inherit (a little pun :stuck_out_tongue:; bad Paulo!) a bunch of maintenance hurdles.

The Logtalk version of my example is arguably cleaner:

:- object(rules).

	:- public([rule/1, fact/1]).

	rule(X) :-
		transform1(X).

	transform1(X) :- 
		Y is X * 2,
		transform2(Y).

	transform2(X) :-
		Y is abs(X),
		% call fact/1 in "self"
		::fact(Y).

:- end_object.

The only place in the code where you need to be context aware is when calling fact/1 (which could also be declared protected or private).

Thank you. I appreciate the pointer. Will check it out when I have some free cycles.

Because predicates, modules and functors (name/arity pairs) are not garbage collected. Eventually that bites Logtalk as well:

r :-
    gensym(n, Name),
    gensym(a, Pred),
    C =.. [Pred,X],
    findall(C, between(1,3,X), Cs),
    create_object(Name, [], [public(Pred/1)], Cs),
    findall(X, Name::C, Ns),
    assertion(Ns == [1,2,3]),
    abolish_object(Name).

Now run this and watch the process using OS tools to see it grow forever.

 ?- forall(between(1, 1000000, _), r).
2 Likes

Yes. a clean solution requires some self notion. Logtalk has that by design. In plain (SWI-)Prolog you have some options, roughly:

  • (Mis)use meta predicates. Problem is that this indeed requires a lot of book keeping, although it isn’t that hard to automate that.
  • Use the SWI-Prolog stack introspection to get some goal on the stack from which we can derive self. This is fairly simple to program and “not so bad”. Can be a little slow if the stacks are deep. It is still a design pattern for which I at some plan want better (means faster and cheaper) support.
  • Notably for this case, where the combination of a thread and a module are used, put the self in a global variable, so you get
    nb_getval(self, Self),
    Self:fact(X).

Here is a demo of the idea to manage self on the stack. Start as

swipl oo.pl facts.pl
?- facts::rule(X).
X = yip.

oo.pl (423 Bytes) facts.pl (66 Bytes) rules.pl (96 Bytes)

Noted. Thanks for clarifying. But giving that dynamic objects are rarely needed and used, this (current) limitation of the SWI-Prolog garbage collector is seldom an issue. When using static objects with some dynamic predicates, the common practice, as in Prolog, is to retract(all) clauses when some (re)initialization is required. This is a relatively more common scenario but, as in Prolog, the use of dynamic predicates is/should be also minimized. Curious about real world applications, if any, where this garbage collection limitation proved to be a significant issue. I guess this can be more of a problem for long running applications doing a large number of dynamic predicate updates.

In that solution, predicates are being forced into being meta-predicates, not because they have meta-arguments, but for the side effect of passing the calling context (i.e. the module from where they are being called; this implies use of implicit qualification in the calling module, otherwise things get even uglier fast). If the predicate have multiple arguments, do your declare all of them as meta-arguments? Just the first? What if the predicate have no arguments? What if one of the arguments is a true meta-argument? How do you distinguish it from other, let’s say, “accidental” meta-arguments? What about developer tools that must rely and trust meta-predicate declarations? How do you automate around all these and other issues? Then, you also need to maintain, document, and test all that automation code. This is clearly not a viable solution for anything but a toy problem.

The issues of that stack based solution, notably regarding its clash with tail recursion, have been discussed here in the past.

I understand the motivation here. But the implicit message here is also more about assimilation and control than actual cooperation.

This requires setting the self global (but thread local) variable anytime a predicate from another module is called. Fail to do it in a single point in the code and the whole thing can collapse. More housekeeping although possibly more amenable to automation that have a chance of actually working compared to the first solution.

It is indeed not that often an issue. Note that clauses are handled properly. The problem appears if a server is supposed to handle a huge or infinite series of arbitrary programs that may each use a different terminology (=predicates). SWISH is one such application and that is why I added true temporary modules. As these are currently unsafe for general use this is just a hack. Luckily most SWISH users follow similar courses and thus the remaining leak of name/arity pairs is not a big issue :slight_smile: Several people have expressed they want to realise a scenario with very many different programs. I guess in most cases creating a module and abolishing all its predicates does pile up a bit of garbage, but is still acceptable.

attempts at self

You are right on these things in general. Note that loosing LCO for :: is probably not that much of a problem in most cases. (Deep) recursion typically doesn’t happen over ::. All the others have indeed there limitations. They are probably pretty adequate for the problem sketched by the OP though.

Note that your code assumes that the predicates called in self are actually defined in self. But messages to self actually lookup along the inheritance path (which you try to emulate using the use_module(rules) directive in facts). Simple and quite typical example based on your code:

:- module(rules,
          [ rule/2
          ]).
:- use_module(oo).

rule(X, Y) :-
    ::fact1(X),
	::fact2(Y).

% defaults
fact1(1).
fact2(2).
:- module(facts,
          []).
:- use_module(rules).

% override inherited
fact1(3).
$ swipl oo.pl facts.pl
...

?- facts::rule(X, Y).
ERROR: Unknown procedure: facts:fact2/1
ERROR:   However, there are definitions for:
ERROR:         facts:fact1/1
ERROR: 
ERROR: In:
ERROR:   [13] facts:fact2(_20630)
ERROR:   [12] oo: ::fact2(_20664) at /Users/pmoura/Downloads/jan/oo.pl:25
ERROR:   [10] oo:facts::rule(3,_20702) at /Users/pmoura/Downloads/jan/oo.pl:13
ERROR:    [9] <user>
ERROR: 
ERROR: Note: some frames are missing due to last-call optimization.
ERROR: Re-run your program in debug mode (:- debug.) to get more detail.
   Exception: (13) facts:fact2(_10274) ? 

As the saying goes, “For every complex problem there is an answer that is clear, simple, and wrong”. The problem, in this case, is not that complex but it does require more than just looking up a frame in the stack. That said, limited solutions can still be useful in carefully controlled usage scenarios.

SWI-Prolog isn’t Logtalk. These are just building blocks that allow implementing one aspect of OO programming. There is a lot of functionality, notably in the module system, that can take this further. If one really wants OO with Prolog, Logtalk is certainly the way to go. If one is generally happy with the module system as organizing system, but some OO aspects are required somewhere in the application these primitives can help out.

22 years after Logtalk was released, you still don’t understand it. Let me fix that sentence for you: If one really wants code encapsulation and code reuse in Prolog, Logtalk is certainly the way to go.

I do understand that the idea of replacing modules with something that subsumes their functionality while providing much more is a painful one considering that most of the SWI-Prolog ecosystem is built on top of its module system. But it’s not like I started showing an alternative only yesterday. Or that I made it difficult for Prolog implementers and vendors to actually try out Logtalk using their own systems.

Meanwhile, every single time I provide a simple, elegant, and working solution to a user in trouble trying to twist modules into something that they are not designed for, we get this barrage of half-broken hacks. How is that helpful? How is keeping users writing non-trivial applications frustrated with the module system and writing more and more convoluted code the way forward?

I’m in favor of ending this discussion somehow. It is a standard repetition of moves

  1. User has some code reuse, module or related problem
  2. @pmoura shows how to do it with Logtalk
  3. (Often) I show you can also do this with SWI-Prolog (modules)
  4. We end up comparing Logtalk with modules.

Notably (4) is not productive (anymore) and possibly (likely?) makes the OP wondering to what community s/he turned. Let us (try to) stop (4).

(2-3) is a bit harder. As far as I’m concerned Paulo has the right to show his X ( :slight_smile: ) can solve the problem cleanly. Showing the problem can also be solved using SWI-Prolog’s native libraries has (IMO) its value too. After all, this is a SWI-Prolog forum.

There will always be people strongly believing in either solution and such a fight comparing systems/languages/… rarely has winners. There are simply plenty arguments to see it either way. If needed, we can assemble such a list and put it in some corner of this forum so we can refer to it.

1 Like

This misses the main point in my previous post. Or rather, avoids it.

Us vs them. Artificial fences. But message received loud and clear. I have been contributing and attempting to build bridges and foster actual cooperation for two decades. Two decades of essentially one side contributions. No patience or motivation left. I will continue to think different elsewhere. Unsubscribed.

@pmoura I would suggest to please don’t make a rash decision. Jan said that your Logtalk contributions are valuable and I think all the community here appreciates your comments (even Jan).

You are an important contributor to the Prolog community, so I am humbly asking you to stay, thereby preventing another division in the prolog community. We all get along very well here, please reconsider the decision since it will be a loss for all of us.

If you don’t want to stay for your own sake, please stay for the sake of the rest of us.

8 Likes

I thank you both for your helpful viewpoints. Both are helpful.