skip to content

Two recursive containment rules have identical logical readings, but one fails to terminate - how can clause and goal order decide that?

level: seniorimportance: nice to knowfreq 29%

answer

  1. meaning is order-free, execution is not
  2. logic plus control, not logic alone
  3. name what shrinks each recursive call
  4. recursive goal before the grounding goal
  5. cycles need a visited set

basics

~20 s

Order changes nothing about what the rules mean and everything about how the engine looks for it. Under a fixed depth-first, top-to-bottom strategy, a recursive goal placed before the goal that would narrow it descends forever.

solid answer

~50 s

This is Kowalski's **algorithm = logic + control**: you write the logic, the engine supplies the control, and only their combination decides termination. The declarative reading of a clause is order-free - conjunction commutes - but the engine is not. It takes goals left to right, clauses top to bottom, depth first. Put the recursive goal first and it is re-entered with the same arguments and nothing further bound, so each level is as general as the one above it and the descent never bottoms out. Put the fact goal first and each recursive call is made on something already narrowed, so the search shrinks and ends. Cyclic data breaks even a well-ordered rule unless you carry the components already visited, and engines differ in whether they memoise repeated goals, which changes which programs terminate at all.

code

pseudocode · 16 lines
pseudocode
rule partOf(Whole, Part):
    contains(Whole, Part)

# variant A - recursive goal first
rule partOf(Whole, Part):
    partOf(Whole, Middle)      # same Whole, nothing further bound
    contains(Middle, Part)

# variant B - grounding goal first
rule partOf(Whole, Part):
    contains(Whole, Middle)    # Middle bound by a fact
    partOf(Middle, Part)

# identical declarative reading; under a depth-first engine variant A
# returns the answers reachable through the base clause and then
# descends forever once they are exhausted

go deeper

for a junior

Take away the headline: the order of goals does not change what a rule means, but it does change whether the engine ever finishes looking.

for a middle

Explain the two readings of a clause and show, on a recursive rule, which goal has to run first so each recursive call is made on something already bound.

for a senior

Diagnose it in production terms: name what shrinks per recursive call, distinguish an ordering bug from cyclic data, and say why the hang surfaces on the query with no answer.

for a principal

Treat it as an engine-dependence question - the same clauses terminate on a memoising engine and hang on a plain one, so the choice of engine is part of the program's contract.

## Two readings of the same clause Every clause in a logic program has two readings, and this question lives in the gap between them. The **declarative reading** says what the clause means: the head holds whenever the body goals all hold. Conjunction is commutative, so reordering the body changes nothing about the set of true instances. Two clauses in either program order define exactly the same relation. The **procedural reading** says what the engine will do: solve the goals left to right, try clauses top to bottom, descend depth first, and backtrack on failure. That reading is highly order-sensitive. Kowalski's formulation is the standard name for this split - **algorithm = logic + control**. The programmer supplies the logic; the engine supplies a fixed control strategy; termination and cost are properties of the pair, not of either alone. It is why a logic program can be perfectly correct as a specification and useless as a program. ## Why the recursive-goal-first variant diverges Consider the transitive closure of containment written both ways, with the same non-recursive base clause in each. In the variant whose body begins with the recursive goal, solving `partOf(cabinet, X)` immediately raises `partOf(cabinet, Middle)` - the **same** first argument, and a second argument that is, if anything, less constrained than before. That subgoal raises the same subgoal again, forever. Nothing in the descent gets smaller, and the engine has no notion that it has seen this goal before. The important nuance, and the thing an interviewer will push on, is *when* the divergence shows up. If the base clause is tried first, the query happily prints the answers reachable through it and only loops when those are exhausted - that is, when you ask for one answer too many, or when you pose a query that has no answer. A program that 'works' in a demo and hangs the first time a part is genuinely absent is exactly this defect. In the variant whose body begins with the fact goal, the recursive call is made with an argument that a fact has already bound, so each level descends one real step into finite data and the search terminates. ## Cycles are a separate failure Ordering fixes the shape of the recursion; it does not fix the data. If the facts contain a cycle - a housing recorded as containing a bracket that is recorded as containing the housing - a correctly ordered rule still walks round it forever, because each step is a genuine step and there is always another one. The usual repair is to thread the set of components already visited on this branch through the recursion and refuse to re-enter one. That repair is worth naming honestly: it is **control leaking into the logic**. The visited set says nothing about what containment means; it exists solely to steer the engine's search. A reviewer who sees an accumulator like that in a rule should read it as a signal that the declarative reading and the operational one have come apart. ## What actually changes termination | lever | changes the meaning? | changes termination? | |---|---|---| | swapping two goals in a body | no | yes | | swapping two clauses of a relation | no | yes, and answer order too | | adding a visited-set argument | yes, slightly - the relation now takes it | yes, on cyclic data | | the engine's search strategy | no | yes, and it is not yours to choose | That last row matters. Engines differ: some memoise goals, answering a repeated goal from a table rather than re-deriving it, and such an engine terminates on programs that a plain depth-first one loops on - including left-recursive definitions and cyclic data. Some offer bounded or iteratively deepened search. So 'does this program terminate' is not answerable from the clauses alone; it is answerable from the clauses plus the engine. ## How to work the problem in practice 1. Read the clause declaratively first and confirm the relation is the one you meant. If the logic is wrong, no ordering saves it. 2. Then read it procedurally: for each recursive goal, name what is strictly more constrained than in the caller. If you cannot name anything, the descent is unbounded. 3. Reorder so that a goal which consults the facts runs before the recursive goal, so every recursive call is made on something bound. 4. Ask whether the data can cycle. If it can, add the visited set and accept that it is control, not logic. The broader lesson generalises past this paradigm: whenever a language lets you state intent and hands execution to an engine, correctness of the statement is not sufficiency. Something still chooses the order, and if you cannot say what that something does, you cannot predict whether your program finishes.

  • Why does a left-recursive definition sometimes print correct answers before hanging?
    Because the base clause is tried first and yields everything reachable through it. The runaway descent only begins when the engine backtracks into the recursive clause after those answers are exhausted - so the failure surfaces on the query that has no answer, not on the demo that does.
  • Does adding a visited-set argument change the relation you defined?
    Technically yes - the relation now carries an extra argument that has nothing to do with containment, and callers must supply it. That is why it is best read as control smuggled into the logic: it exists to steer the engine's search, not to say anything about what being part of something means.
  • Can you decide termination from the clauses alone?
    No. Termination is a property of the clauses plus the engine's strategy. An engine that memoises goals and answers a repeat from a table terminates on definitions that a plain depth-first engine loops on, so the same program can finish on one and hang on another.

saying these in an interview costs you the question

  • Says reordering the body changes what the rule means
  • Thinks a base clause written first is enough to guarantee termination
  • Assumes the engine detects that it has already seen a goal
  • Believes correct logic implies a terminating program
  • Treats cyclic data as impossible rather than as a case to handle