Is there a linter for prolog programs?

Here is an attempt:

In general, with an output argument (mode -), the caller must pass a variable for that argument, and the predicate would backtrack over the possible solutions, i.e. enumerate them. Next, we want also to allow the caller to pass an instantiated or partially instantiated argument: e.g. partially instantiated to filter on the enumerated solutions and only get those that match the given partial term; or maybe fully instantiated, to just check that some term constitutes a valid solution for our predicate; etc.

And that’s where steadfast (mode ?) comes into play: the annotation means exactly that we can pass a (fully or) partially instantiated argument and obtain the behaviour described above, filtering/checking and similar. But the annotation/declaration per se only expresses an intent, we need to code our predicate in such a way that it does work as intended with the partially instantiated arguments, there is no automagic there.

And that is it, a concept more than a pattern, because how to do that concretely quite depends on the specific predicate and code at hand…

That said, the following I think is the most typical example, and the simplest at that:

%! do_some(+, -) is ...
%! my_pred(+, -) is ...
my_pred(X, Y) :- do_some(X, Y), !.

must become:

%! do_some(+, -) is ...
%! my_pred(+, ?) is ...
my_pred(X, Y) :- do_some(X, Y_), !, Y = Y_.

Here is a somewhat less trivial example I have found in my code (on Gist):

%!	unfold_f(+X0, ?X) is det.
%	
%	Unfolds all notations in formula `X0` to `X`.
%	
%	Unfolds recursively on subterms of `X0`
%	by the first matching clause of `notation/2`.

unfold_f(X0, X) :-  % (future-safe: evars)
	var(X0), !, X = X0.
unfold_f(X0, X) :-
	atomic(X0), !, X = X0.
unfold_f(X0, X) :-
	notation(X0, X1), !,
	unfold_f__do(X1, X).
unfold_f(X0, X) :-
	unfold_f__do(X0, X).

%	unfold_f__do(+X0, ?X) is det.

unfold_f__do(X0, X) :-
	X0 =.. [F|X0s],
	unfold_fs(X0s, Xs),
	X =.. [F|Xs].

%!	unfold_fs(+X0s, ?Xs) is det.
%	
%	Unfolds all formulas in list `X0s` to `Xs`.
%	
%	See `unfold_f/2` for more details.

unfold_fs([], []) :- !.
unfold_fs([X0|X0s], [X|Xs]) :-
	unfold_f(X0, X),
	unfold_fs(X0s, Xs).

These are mutually recursive, but that’s immaterial here: the point is the output argument (the second argument) is steadfast, which means these predicates will work correctly (as expected) even when called with partially instantiated arguments (modulo any bugs I have not yet seen): and you can see that the business of binding the actual output after the cut is indeed happening, just now at the leaves of the recursion, i.e. the first two clauses of unfold_f.

Now, those are all examples with det or semidet predicates, hence there is a cut involved and we can work with that. For predicates that might be multi or nondet, think e.g. a nat/1 that is supposed to enumerate but also check the “natural numbers”, we rather typically have to firstly check the type of the input, then branch on that: e.g. if it’s a var do the enumeration, if it’s a structure do something else, otherwise throw; or similar, typically in order to avoid infinite loops, i.e. no solution unifies, keeps retrying forever.

On that, things are in fact complicated by the fact that SWI, as other systems, does argument indexing, so our nat/1 would in fact not need much explicit coding (in SWI) to handle steadfastness on its argument: but argument indexing per se does not solve steadfastness in all cases, so still some thought and often some coding in order to get correct behaviour is needed…

Anyway, to reiterate, that’s all primarily conceptual (specifications and semantics), then somewhat simpler than it sounds: indeed, in my experience, once you start keeping these things in mind in your design and coding, things quickly become clearer: at least the problems, not always the solutions…

1 Like

But how is steadfastness in nondet predicates? Is the handling of attributed variables for instance like clpfd it does also included?
Or predicates which can provide indefinite many solutions like length?

length( L, 3) vs. length( L, LEN), LEN=3

At a first glance it provides at least the same solution.

2 Likes

length/2 is intentionally designed to be helpful, when the length is unspecified. Prolog uses backtracking, so a Length variable which is only instantiated after length/2 has been called, is not in scope (i.e. is unavailable) to length/2. A more obvious example is:

?- format('A equals ~w~n', [A]), A = 5.
A equals _8072
A = 5.

The order of the constraints is critically important, to be able to satisfy the intentions of the program.

The usual intentions of a program are to work “correctly” (i.e. as the programmer intended), and be reasonably performant.

If different behaviour is desired, then we can create our own predicates (e.g. list_length at https://ridgeworks.github.io/clpBNR/CLP_BNR_Guide/CLP_BNR_Guide.html ), or e.g. at least prevent going off into infinity:

?- between(2, 3, Len), length(List, Len).
Len = 2,
List = [_, _] ;
Len = 3,
List = [_, _, _].

So, “at first glance” is a desirable aim for program understandability, but sometimes unachievable because there is always the implicit hard requirement that the person already appreciates the foibles of the language - e.g. compare/3 when given vars:

?- compare(C, A, B).
C = (<).
1 Like

Thanks. This is an interesting case. And no, according to the definition length/2 is not steadfast. Yes, it produces the correct solution, but (1) it does so much slower than when called as length(L, 3) and (2) it runs out of stack on backtracking rather than being deterministic.

In my current view on modes, I think it should be descriped as

  • length(+list, =)
  • length(=, +integer)
  • length(-, -).

I.e., if the first argument is a list, it is steadfast in the 2nd and visa versa. Otherwise it enumerates pairs of lists and integers.

length/2 is a pretty complicated predicate. Just try implementing it correctly in Prolog, providing all solutions when needed and determinism when possible :slight_smile:

1 Like

But we can make it steadfast :

?- when( (nonvar(L); ground(LEN) ), length( L, LEN)), member( LEN, [2,3]).
L = [_, _],
LEN = 2 ;
L = [_, _, _],
LEN = 3.

Does that fit the definition?

This goes off into infinity, though:

?- when( (nonvar(L); ground(LEN) ), length( L, LEN)), L = [_|_], member( LEN, [2,3]).

… because nonvar(L) is triggered by the L = [_|_].

This works better:

?- when( ground(LEN), length( L, LEN)), L = [_|_], member( LEN, [2,3]).
LEN = 2,
L = [_, _] ;
LEN = 3,
L = [_, _, _].

Or this way:

?- when( (nonvar(L); ground(LEN) ), ( ( nonvar(L), is_list( L); \+nonvar(L)), length( L, LEN))), L=[_|_], member( LEN, [2,3]).
false.

?- when( (nonvar(L); ground(LEN) ), ( ( nonvar(L), is_list( L); \+nonvar(L)), length( L, LEN))), member( LEN, [2,3]).
L = [_, _],
LEN = 2 ;
L = [_, _, _],
LEN = 3.

Because is_list( [_|_]) is false.

I really wish there was a way to turn this (variable age) off at will. It messes up with sorting and I have to special-code it every single time. In particular it’s a big pain when I’m trying to compare sets of clauses for (syntactic) equality, which I often have to do when I need to get rid of duplicate programs.

A surprise extra-logical bit of Prolog there.

IIANM, you are looking for comparison “up to alpha-equivalence”, i.e. modulo the name of variables.

A way to do it is using numbervars/*, though I don’t know if there are more direct ways to do it in SWI:

?- numbervars(A), numbervars(B), compare(Ord, A, B).
A = B, B = A,
Ord = (=).

Maybe we need a version of compare which returns the =@=, @<, … class of operators. But that leads to further questions. How would you (m)sort a list which contains A, A and B? Even numbervars cannot help here because the outcome depends on which variable was found first. But at least A and A would be grouped together at one side or the other.

This is indeed one way I use to get around the variable age in sorting. Another is to custom sort with predsort/3.

What you seem to be looking at is ordering that is based on the notion of variance?
If you just want equality, =@=/2 is your friend (or the more widely implemented equivalent variant/2).

If you want the real ordering,

is indeed the way to go. That doesn’t combine with sort/2, so you need predsort/3. If the purpose is maintaining a set, library(hashtable) could be an option. It uses variant_hash/2

Thanks. I’ve used =@=/2 as well as numbervars/3 and unifiable/3 with predsort/3 at different times for slightly different purposes. Somehow not all of them work all the time, but that’s I guess specific to my varied use cases.