Once/1 should be deprecated!

Guys please discuss once/1 and friends here. I even don’t have an example
where once/1 is needed, yet there is alternative solution without once/1.

Edit 26.04.2021:
Ok a use case could be that it is frowned upon once/1+member/2, whereas
memberchk/2 would be ok? But then we dont have a nth0chk/3, etc…

To venture an idea relevant in my code:

I guess, once/1 seems to play two roles:

During forward processing to indicate that truth of a goal or query depends on proving the existence of one solution. Wouldn’t that be the paradigmatic case for once/1.

And during backwards process – backtracking – to avoid redoing the goal — since it proved to be true already. Is this actually happening?

And, i guess, a once should eliminate choice points … but should also not make the goal wrapped in the once fail upon backtracking – ideally, it should also not be called again (hence once)

Not sure I am thinking straight here :slight_smile:

Dan

Thanks for splitting the topic. Yes, you use once if you want a first solution of an otherwise nondet goal. This is in my experience hardly ever the case though. In a deterministic context one typically uses (semi-)deterministic sub-goals. There can be some exceptions, such as handling a line “Var=Value”. Now you could do

line_var_value(Line, Var, Value) :-
    once(sub_atom(Line, B, _, A, =)),
    sub_atom(Line, 0, B, _, Var),
    sub_atom(Line, _, A, 0, Value).

I think the above is perfectly ok code, although in practice I’d write

line_var_value(Line, Var, Value) :-
    (   sub_atom(Line, B, _, A, =))
    ->  sub_atom(Line, 0, B, _, Var),
        sub_atom(Line, _, A, 0, Value).
    ;   syntax_error(invalid_var_decl)
    ).

For example, all libraries and packages use once/1 68 times in 300Klines (that includes the definition and probably some more false hits), mostly in old(er) code.

1 Like

I think you recently posted that Goal -> true was a better performing equivalent to once(Goal) and, using vm_list and measuring the execution time, that indeed appears to be the case.

However, with a little love from the compiler, it would seem that the once form could be slightly better than the -> syntax.

Although it may not be used much, I prefer once to ->. It’s just easier to read and doesn’t have the slightly quirky semantics of ->. But (I think) I understand the performance advantages of the current -> .

library(apply_macros) causes once/1 to be rewritten as (Goal->true). That should give you the best of both worlds. And yes, in theory it could be a bit smarter providing direct VM support. That is more likely to make the system bigger and slower and harder to maintain than gain anything. The work by @dmchurch is likely to make profiling the VM a lot easier and might give more insight in the effect of specialized VM instructions. I think it pretty unlikely once/1 is a good candidate though.

Yes, that would do the job, although I couldn’t find any doc so I had to dig into the development repository. I agree that there would be little justification for specialized VMI’s for once.

But it does seem a bit arbitrary that ->, ;, and \+ all get special compiler treatment and once doesn’t; they’re all ISO control predicates.

Alternatively, why shouldn’t apply_macros (or at least the subset for system primitives) be the default in SWIP rather than an option that requires programmer intervention?

Aside: is this performance issue the only reason why the suggestion that once/1 be deprecated?

once/1 and \+/1 are only bootstrapped. They don’t have much definition in the ISO core standard. They are also not subject to body conversion. They are bootstrapped as:

once(X) :- X, !.

\+(X) :- X, !, fail.
\+(_).

If you use (X->true;fail) (or the shorter (X->true)), instead of once(X), you get a slightly different behaviour, concerning body conversion. You can try yourself:

?- [user].
|: test(X) :- once(X).
|: test(X) :- (X -> true; fail).
|: 
% user://1 compiled 0.02 sec, 2 clauses
true.

?- listing(test/1).
test(A) :-
    once(A).
test(A) :-
    (   call(A)
    ->  true
    ;   fail
    ).

The Prolog interpreter inserted a call/1 into the if-then-else. This is ISO core standard behaviour. Such an insert is not defined for once/1, since this does not follow from its bootstrapping.

The library library(apply_macros) might need to consider such subleties.

Since ($)/0 can set a flag and influence the clause end, this could open up a totally new optimization. Namely last call optimization for a onced call. But its difficult, possibly the flag needs to be set inside the goal argument execution.

Currently last call optimization for onced goals depends on what the system predicate once/1 does with the goal argument. And it is indeed bootstrapped in SWI-Prolog, and doesn’t do last call optimization. There is no i_depart or some such.

Here you see how it is bootstrapped:

?- listing(once/1).
:- meta_predicate once(0).

system:once(Goal) :-
    call(Goal),
    !.
true.

And here you see that the Prolog VM cannot apply last call, because the cut (!)/0 is the last goal:

?- vm_list(once/1).
========================================================================
once/1
========================================================================
       0 s_lmqual(0)
       2 s_trustme(clause(105553166890112))
----------------------------------------
clause 1 (<clause>(0x600003047480)):
----------------------------------------
       0 i_enter
       1 b_var0
       2 i_usercall0
       3 i_cut
       4 i_exit
true.

Same problem if you convert once/1 into Goal->true. Doesn’t improve the situation. You will see a c_cut(1) instruction for a light weight choice point, instead of an i_cut for a heavy weight choice point (which isnt there because s_trustme, the call frame determines what is removed).

Both instructions negatively interfer with last call optimization (LCO).

Eventually we need a more general inline strategy that is controlled by optimization modes. apply_macros is a crude quick first step borrowed from YAP.

1 Like

Perhaps, although in this particular case (not that it’s that compelling) I would argue that the compiler should already be optimizing as it does for -> and \+. Probably one reason it doesn’t is that it isn’t worthwhile to add a special VMI for it, since a slightly sub-optimal form can be built using ->.

Aside: The really compelling use case for me is something like once(N<0 ; N>1) when arithmetic is optimized; much more natural IMO than (N<0 -> true ; N>1).

I have no issue with a more general inline strategy as long as it doesn’t violate the “perfect is the enemy of good enough” principle.