Guarded and unguarded goal calls

Hello,

I am developing an api with predicates. The api would be called by client code. However, the api is also called “internally”.

To ensure that client code is called with correct argument types, the arguments are guarded by lookup calls.

goal(Arg1, Arg2) :-
    check(Arg1),
    check(Arg2),
    do_processing(Arg1, Arg2).

However, when the same goals are called internally, those guarded lookup calls are redundant – they already happened, but now, they are executed again.

do_processing(Arg1, Arg2) :-
    do_processing(Arg1),
    goal(Arg1, Arg2).

Short to creating two parallel api’s of guarded and unguarded calls – is there a better way to specify or detect that when an api is called “externally” it should be guarded and when called internally, guards can be skipped?

goal_unguarded(Arg1, Arg2) :-
    do_processing(Arg1, Arg2).

Any thoughts are much appreciated.

Dan

What’s wrong with this:

goal(Arg1, Arg2) :-
    check(Arg1),
    check(Arg2),
    goal_impl(Arg1, Arg2).

goal_impl(Arg1, Arg2) :-
   ....

And just call goal_impl/2 internally. The cost of making that call from goal/2 is quite small.

You can possibly avoid the goal_impl/2 stuff by using one module for the external API and another module for the internal API, with something like :-use_module(internal, [goal_impl/2 as goal]).

Another possibility is to use wrap_predicate/4 to add the checks in some places. For an example of using “wraps”, see the implementation of the rdet pack.

1 Like

Hi Peter,

Yes, that is one strategy that I am doing, and for lower level code where time is critical I was hoping to avoid the extra call.

I guess wrap predicate is what i am looking for – and the test is to somehow know if the call originated internally or externally.

Ideally, i was hoping that such a test could be achieved during compile time, and the effect achieved through some code generation capability.

The solution you suggest is static in nature and whether a call is external or internal could be simple to determine (a simple module or goal name lookup, perhaps).

Dan

My $0.001 are these rules:

  • Do not insert checks when you call some built-in or library that will do the check anyway. For example, if you pass an argument to the 2nd argument of arg/3, there is no point checking it is a compound. The arg/3 call will trap it soon after and the stack trace will show what is going on.
  • Only add additional checks when there is a fair chance that users will call the predicate with wrong arguments. Otherwise it mostly clobbers the code.
  • If you decide you need an additional check, use must_be/2 for checks that do not significantly affect performance and assertion/1 for more expensive checks so you can disable them using swipl -O.
  • If you really think type checks are needed on all APIs, use some declaration. There are several packages around, some based on using the PlDoc annotations, the restricted Ciao assertion language by @edison, the type checker by Tom Schrijvers, etc. Type checking based on declarations is easily only enabled during debugging and can also be used for static analysis. The wrap_predicate/4 API can be used to (de)activate checks at any time.

I normally only add minimal checks in after getting into a situation that proves to be hard to debug. I’m also a big user of trace/1 and trace/2 to search for failing predicates that are not supposed to fail.

1 Like

Hi Jan,

Thank you. Great guidance.

Some checks have a dual purpose – to enable making the call with unbound arguments as well, so they serve as lookups, to generate solutions.

Although, I noticed that these can become very expensive to use and the value for the client code is currently unclear.

I guess client code can always wrap predicates if that is the kind of behavior needed.

Dan

If the generator is expensive, use e.g.,

(var(X) -> gen(X) ; true)

If it is a simple lookup of a fact, just calling is probably fine.

Some generators are fast at checking the result, in which case, just gen(X) without the var(X) test suffices. (And if you’re doing a generate-and-test algorithm, it can often be sped up by creating delayed test goals first and doing the generator second.)