Pure relations explore every alternative. Real programs sometimes need to keep only the first answer, make a local if-then-else decision, or turn an exception into an ordinary result. This example shows the ISO control constructs for each of those, on a tiny route table.
Run it in the playground · Source
From Antwerp there are routes to Ghent and Brussels. What is the first destination? Is there a route to Ghent, and to Paris? And when a caller insists on a route that does not exist, can the program recover gracefully?
route(antwerp, ghent).
route(antwerp, brussels).
first_destination(From, To) :-
once(route(From, To)).
preferred_destination(From, To) :-
route(From, To),
!.
travel_status(From, To, Status) :-
(call(route(From, To)) -> Status = connected ; Status = disconnected).
once/1 keeps the first solution of a goal. The cut ! discards the
remaining alternatives of the predicate. If-then-else (-> ;) commits to the
first solution of its condition and otherwise takes the else branch.
call/1 runs a goal held as data.
require_route(From, To) :-
(route(From, To) -> true ; throw(no_route(From, To))).
checked_route(From, To, Result) :-
catch(
(require_route(From, To), Result = accepted),
no_route(From, To),
Result = rejected
).
A missing route that the caller requires is not a plain “no”: it throws
no_route(From, To). catch/3 unifies the thrown term with its pattern and
runs the recovery goal instead.
eyeprolog examples/iso-control-and-errors.pl
report(first_destination, ghent).
report(cut_destination, ghent).
report(existing_route, connected).
report(missing_route, disconnected).
report(recovered_exception, rejected).
Both once/1 and the cut keep Ghent and drop Brussels. The Paris route is
disconnected, and the exception for it is turned into rejected.
eyeprolog --proof examples/iso-control-and-errors.pl
step(checked_route(antwerp, paris, rejected),
rule(7),
['From' = antwerp, 'To' = paris, 'Result' = rejected],
[catch((require_route(antwerp, paris), rejected = accepted), no_route(antwerp, paris), rejected = rejected)]).
rejected comes from rule 7, whose single use is the whole catch/3 call
with the bindings that held when it succeeded. The proof records that the
catch produced rejected, not the thrown exception inside it.
condition('C1', resolution, ok, 11).
condition('C5', re_decision, ok, 7).
obligation(absent, theory_scoped, \+ call(route(antwerp, paris))).
obligation(builtin, theory_scoped, catch((require_route(antwerp, paris), rejected = accepted), no_route(antwerp, paris), rejected = rejected)).
verdict(checked_with_obligations).
Eleven steps replay against their clauses. The two if-then-else decisions are
recorded as the branch each took and composed from its goals. The branch taken
when there is no route rests on \+ call(route(antwerp, paris)), which depends
on the program’s own route/2 facts, and so does the catch/3: the checker
cannot recompute them in isolation. They are recorded as obligations:
trusted boundaries named in the report.
report(all_destinations, L) :- findall(To, route(antwerp, To), L).
It lists both Ghent and Brussels, which once/1 and the cut drop.checked_route/3 to no_route(_, ghent). The
exception no longer matches, and the run ends with an uncaught
no_route(antwerp, paris).route/2 facts and predict the first two reports.Learn more in The Art of EyeProlog, Chapter 34.