Why no if-but-not-else construct?

I often find myself using the (P -> Q; true) formulation, when I need to do an extra check or take an extra action but only when a certain precondition is true; when it isn’t, the clause can proceed as it would otherwise. Why isn’t there a built-in operator or predicate that does this? Perhaps something that binds tighter than ,/2, like say:

:- op(800, xfy, ?).

% Whenever P succeeds, then Q must succeed, but \+P is fine
P ? Q :- P *-> Q; true.

% Match *or* generate "(At)" in text, when At is an atom
parenthesized_atom(At) -->
    `(`,
    {ground(At) ? atom_codes(At,AC)},
    string(AC),
    `)`,
    {atom_codes(At,AC)}.

Note that this isn’t a “how can I do this, please” - I have a number of ways I can implement this, and it’s not all that cumbersome (and more to the point, I want to make sure that I am writing idiomatic Prolog). It’s more of a “why was Prolog designed this way?” combined with some amount of “are there hidden pitfalls to doing this kind of thing that I’m not aware of?”

I don’t know the reason (I have a couple of theories), but I do know that Logtalk’s linter complains about (P->Q) but doesn’t complain about (P->Q;fail).

Paulo Moura (author of Logtalk) doesn’t seem to be in this discussion group, but maybe somebody knows the reason?

I think the reason is that from a logical point of view, P -> Q is seen as (in logic) P implies Q.

Now, when P implies Q (in logic), you are not saying anything at all about what happens if not P (you could still have Q even if not P), and therefore I think it would be sensible from this point of view to always specify what happens if not P when you are implementing a programming language.

Almost every time I use (P->Q;true), Q is something “impure”, such as a print statement – I have more examples of (P->true;Q).

So, maybe it’s a Good Thing that there’s no short-cut for (P->Q;true) because it’s a “code smell”.

Yes, I don’t often need this construct.

On the other hand (P->true;Q) is much more common because P unifies what needs to be unified.

Sometimes when I didn’t write any code, its just my feets that smell.
Here is a typical usecase for that the pattern (P->Q;true):

    ...
    (X < 0 -> throw(error(domain_error(not_less_than_zero,X),_)); 
     true),
    ...

You could inline must_be/2 that way.

1 Like

(bad_input(X)->throw(error(bad_input(X), _));true) … this is similar to must_be/2 or domain_error/2.
Although there’s been a long discussion about failure vs throwing an exception.

Another useful case: In a DCG: ({some_test(X)}->[X];[]).

The Logtalk style check probably stems from the fact there are indeed a few Prolog systems, maybe through a Prolog flag, that can interpret (A → B) as (A → B; true) and not as (A → B; fail). You might need to ask Paulo Moura or Ulrich Neumerkel for an example of such a Prolog system,

I don’t remember a name just now. What would be the motivation for having a (A → B; true) semantics of a trailing (A → B) in a disjunction? Well it would match what imperative languages do. In imperative languages we can do:

IF X < 0 THEN
     throw(error(domain_error(not_less_than_zero,X)
END

We do not need to write:

IF X < 0 THEN
     throw(error(domain_error(not_less_than_zero,X)
ELSE 
     skip;
END

In an imperative language usually an IF without an ELSE branch does not “abort” when the IF condition fails. On the other hand Prolog (X < 0 -> throw(...)) would have this semantics, where “abort” means execution fails:

IF X < 0 THEN
     throw(error(domain_error(not_less_than_zero,X)
ELSE 
     abort;
END

Besides increasing portability, since there exist Prolog systems which interpret the trailing (A → B) like in an imperative language, contrary to the ISO core standard, the Logtalk style check also helps beginners, that might erroneously expect a Prolog system to interpret a trailing (A → B)

like in an imperative language, whereas by the ISO core standard semantics it doesn’t.

But if-then-else is a can of worms. It might generate some headache
in Prolog systems that do aggressive conjunction flattening or even
attempt disjunction flattening.

Quizz, whats the result of these in terms of choice points.
Can you answer without running SWI-Prolog?

?- ((true->true),true;true).

?- ((true->true);true).

?- ((true->true);true;(true->true)).

?- (((true->true);true);(true->true)).

Ooh fun, I’ll play! Let’s see. I’ll mark the trues that get executed with %%%%

?- ((true->true),true;true).
%    %%%%  %%%%  %%%%^ Choice point here, this isn't a ->; construct
?- ((true->true);true).
%    %%%%  %%%%        No choice points, the -> cuts the ; choice
?- ((true->true);true;(true->true)).
%    %%%%  %%%%      ^ Choice point here, the second ; doesn't get cut
?- (((true->true);true);(true->true)).
%     %%%%  %%%%       ^ Choice point here, syntactically equivalent to the above

I think these are correct. Not so sure about #3, but I’m pretty sure the precedence rules group on the left ;/2, so only that one gets cut by the ->/2.

Well? How’d I do? :grinning_face_with_smiling_eyes:

3 (Nr 1, Nr 2 and Nr 4) correct, 1 (Nr 3) wrong:
Solutions are here:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
v

?- ((true->true),true;true).
true ;
true.
?- ((true->true);true).
true.
?- ((true->true);true;(true->true)).
true.
?- (((true->true);true);(true->true)).
true ;
true.

Ah! I had my precedence rules for ;/2 backward. I always have trouble with those… which is why write_canonical/1 is my best friend :grinning_face_with_smiling_eyes:

(;)/2 is xfy, so associativity lets read (a;b;c) as (a;(b;c)).
So if a has (->)/2 it also cuts away b and c.

1 Like