Prolog Coding Horror
Prolog Coding Horror
I seemed to hear the whispered cry, "The horror! The horror!"
(Joseph Conrad, Heart of Darkness)
Why are you here?
As a Prolog programmer, you likely have a rebellious streak<br>in you. In many cases, this is what it takes to guide you<br>away from how an entire industry currently tries to solve<br>problems. To focus on what lies beyond.
The point of this page is to show you where following this streak<br>is likely not a good idea because the cost<br>is high, and there is no benefit to it.
A small number of rules suffices to write great<br>Prolog code. Breaking them will result in programs that are<br>defective in one or more ways.
Video:
The horror: Losing solutions
A Prolog program that terminates and is acceptably<br>efficient can be defective in two major ways:
It reports wrong answers.
It fails to report intended solutions.
Which of these cases is worse? Think about this!
Suppose a program is defective only in the first way. Is<br>there anything you can do to still obtain only correct results?<br>Then, suppose a program is defective only in the second<br>way. What are your options to somehow still obtain all solutions<br>that were intended?
The primary means to make your programs defective in<br>the second way is to use impure<br>and non-monotonic language constructs. Examples of this<br>are !/0, (->)/2 and var/1. A<br>declarative way out is to use clean<br>data structures, constraints<br>like dif/2, and<br>meta-predicates<br>like if_/3 .
The horror: Global state
As a beginner, you will be tempted to modify<br>the global database in Prolog. This<br>introduces implicit dependencies within your programs.<br>By "implicit", I mean that there is nothing in your program<br>that enforces these dependencies. For example, if you use<br>such predicates in a different order than intended, they may<br>unexpectedly fail or yield strange results.
The primary means to make your programs defective in this way is<br>to use predicates like assertz/1<br>and retract/1. A declarative way out is to use<br>predicate arguments<br>or semicontext notation to<br>thread the state through.
The horror: Impure output
As a beginner, you will sometimes be tempted to print<br>answers on the system terminal instead of letting the toplevel<br>report them. For example, your programs may contain code<br>like this:
solve :-<br>solution(S),<br>format("the solution is: ~q\n", [S]).
A major drawback of this approach is that you cannot easily<br>reason about such output, since it only occurs on the system<br>terminal and is not available as a Prolog term within your<br>program. Therefore, you will not write test cases for such output,<br>increasing the likelihood of introducing changes that break such<br>predicates. Another severe shortcoming is that this prevents you<br>to use the code as a true relation.
To benefit from the full generality of relations, describe<br>a solution with Prolog code, and let the toplevel do the<br>printing:
solution(S) :-<br>constraint_1(S),<br>etc.
Sometimes, you may want special formatting. In such case, you can<br>still describe the output in a pure way, using for example the<br>nonterminal format_//2 .<br>This makes test cases easy to write.
The horror: Low-level language constructs
Some Prolog programmers may see little reason to use more recent<br>language constructs. For example, CLP(FD) constraints have only<br>been widely available for about 20 years, which is<br>a comparatively recent development for Prolog. If you<br>think that low-level constructs have served you well, why bother<br>learning newer material? The fact that millions of students<br>were not well served by lower-level constructs need not<br>concern you.
Unfortunately, sticking to low-level constructs comes at a high<br>price: It makes the language harder to teach, harder to learn and<br>harder to understand than necessary. It requires students to learn<br>declarative and operational semantics essentially at the same<br>time, which is too much at once in almost all cases.
The primary means to make Prolog harder to teach than necessary is<br>to introduce beginners to low-level predicates for arithmetic<br>like (is)/2, (=:=)/2 and (>)/2. A<br>declarative way out is to teach constraints<br>instead. See declarative integer<br>arithmetic .
Horror factorial
To see some of these defects exemplified, behold the horror<br>factorial :
horror_factorial(0, 1) :- !.<br>horror_factorial(N, F) :-<br>N > 0,<br>N1 is N - 1,<br>horror_factorial(N1, F1),<br>F is N*F1.
Observe the horror of losing solutions when posting<br>the most general query:
?- horror_factorial(N, F).<br>N = 0, F = 1.
The version without !/0 is almost as horrendous:
horror_factorial(0, 1).<br>horror_factorial(N, F) :-<br>N > 0,<br>N1 is N - 1,<br>horror_factorial(N1, F1),<br>F is N*F1.
The horror of low-level language constructs prevails:
?- horror_factorial(N, F).<br>N = 0, F = 1<br>; caught: error(instantiation_error,'(is)'/2)
If you accept this, you are
limited by using outdated language constructs.
mistaking relations for functions.
not caring about the most general query.
preventing declarative debugging by using impure constructs.
A...