← Back to Blogs
HN Story

Avoiding the 'Horrors' of Prolog: A Guide to Declarative Purity

May 18, 2026

Avoiding the 'Horrors' of Prolog: A Guide to Declarative Purity

Prolog is a language that often attracts programmers with a rebellious streak—those looking for a way to solve problems beyond the paradigms of mainstream industry standards. However, this rebellion can lead developers down a path of "coding horrors" if they mistake the language's flexibility for a license to ignore its logical foundations.

Writing great Prolog code doesn't require complex mastery; rather, it requires adhering to a small set of rules that preserve the declarative nature of the language. When these rules are broken, programs become defective—not necessarily by crashing, but by failing to be truly relational.

The Nature of Prolog Defects

A Prolog program that terminates and runs efficiently can still be fundamentally broken in two ways:

  1. Reporting wrong answers: The program provides incorrect results.
  2. Failing to report intended solutions: The program misses valid results.

While the first is obviously problematic, the second is often more insidious. If a program reports wrong answers, you can potentially filter them out. But if a program fails to report a solution that logically exists, there is often no way to recover that lost information without rewriting the logic.

Common Prolog Antipatterns

1. Losing Solutions through Impurity

Many developers rely on impure and non-monotonic constructs to control execution. The most common culprit is the "cut" operator (!/0), along with (->)/2 and var/1. While these can optimize performance or prevent unnecessary backtracking, they often destroy the program's ability to find all valid solutions, turning a relation into a one-way function.

The Declarative Alternative: Use clean data structures, constraints (such as dif/2), and meta-predicates like if_/3 to handle conditional logic without sacrificing purity.

2. The Trap of Global State

Beginners are often tempted to use the global database via assertz/1 and retract/1. This introduces implicit dependencies; the program's correctness becomes dependent on the order of execution rather than the logical relationship between facts.

The Declarative Alternative: Pass state explicitly through predicate arguments or use semicontext notation to "thread" the state through the computation.

3. Impure Output and Side Effects

Printing answers directly to the terminal using format/2 inside a predicate is a common mistake. This prevents the code from being used as a true relation and makes automated testing nearly impossible because the output exists only on the screen, not as a Prolog term.

The Declarative Alternative: Describe the solution in the code and let the toplevel handle the printing. If special formatting is required, use a pure way to describe the output (e.g., format_//2) so that the output remains a testable term.

4. Reliance on Low-Level Constructs

Many programmers cling to outdated, low-level arithmetic predicates like is/2, =:=/2, and >/2. These require the programmer to manage both declarative and operational semantics simultaneously, making the code harder to read and maintain.

The Declarative Alternative: Use Constraint Logic Programming over Finite Domains (CLP(FD)). By teaching and using constraints, the language becomes more intuitive and the programs more general.

Case Study: The Horror Factorial

Consider a typical "horror" implementation of a factorial:

horror_factorial(0, 1) :- !.
horror_factorial(N, F) :- 
    N > 0, 
    N1 is N - 1, 
    horror_factorial(N1, F1), 
    F is N*F1.

This version is defective. If you ask the most general query—?- horror_factorial(N, F).—it will only return N = 0, F = 1 and then stop because of the cut (!). If you remove the cut, you hit the "instantiation error" because is/2 requires its arguments to be instantiated.

By shifting to a pure, constraint-based approach, we get a truly relational program:

n_factorial(0, 1).
n_factorial(N, F) :- 
    N #> 0, 
    N1 #= N - 1, 
    n_factorial(N1, F1), 
    F #= N*F1.

Now, the most general query ?- n_factorial(N, F). will correctly generate all pairs of factorials (N=0, F=1; N=1, F=1; N=2, F=2...) indefinitely.

The Debate: Purity vs. Pragmatism

While the drive toward purity is powerful, it is not without critics. Some experienced practitioners argue that the "horror" label is overblown. As noted in the community discussion:

"I'd categorise his list as 'Things to be careful with' not 'Coding horrors'... I agree that mutating the database in the second phase is probably a bad idea, but that's not the same as saying assertz() always bad."

Others point out that the language provides impure constructs specifically to handle edge cases that would otherwise require complex library imports. The key, as suggested by the Prolog Coding Guidelines, is to use cuts sparingly and precisely, and to avoid assert/retract unless you actually need to preserve information through backtracking.

Conclusion

Rebellion in programming is valuable when it leads to better abstractions, but it is counterproductive when it clings to outdated patterns. By embracing the pure, monotonic subset of Prolog and utilizing modern constraints, developers can create programs that are not only more efficient but are fundamentally more general and logically sound.

References

HN Stories