为了解决这个问题,我们将使用 prolog 解释器。 :)
min(E, [E]) :-
write('case 1: '), write(E), nl.
min(E, [E|L]) :-
write('case 2: '), write(E), write(' '), write([E|L]), nl,
min(F, L),
E =< F.
min(E, [F|L]) :-
write('case 3: '), write(E), write(' '), write([F|L]), nl,
min(E, L),
E =< F.
我们进行查询:
min(E, [2,1]).
(A) Prolog 从第一个子句min(E, [E]) 开始,由于[2,1] 无法与[E] 统一而失败。然后它进入下一个子句min(E, [E|L]),并且能够通过将E 与2 和L 与[1] 统一来将[2,1] 与[E|L] 统一起来,然后我们看到:
case 2: 2 [2,1] % This is E instantiated as 2, and [E|L] as [2|[1]]
(B) Prolog 然后进行递归查询,min(F, [1])。从这里,它回到子句列表的顶部(在新查询中,它从顶部开始)并且能够通过统一F 和1 来统一第一个子句min(E, [E]) 中的变量。然后我们看到:
case 1: 1
(C) 这个查询成功,返回查询它的子句,遇到E =< F,其中E 与2 统一,F 与1 统一。但随后E =< F 将失败,因为1 =< 2 不正确。此时,Prolog 将回溯并重新尝试它刚刚执行的先前递归查询min(F, [1])。回想一下,查询已经执行了第一个子句并成功了,所以现在回溯它将尝试第二个子句。它看起来将min(F, [1]) 与min(E, [E|L]) 统一起来,可以通过将E 与1 和L 与[] 统一来实现。然后第 2 条执行,我们得到:
case 2: 1 [1]
(D) 我们现在是第 2 条深处的另一个调用。我们还没有完成第一个调用。所以这个新的调用将查询min(F, [])(记住L在这种情况下与[]统一)。您的谓词中没有与min(F, []) 匹配的子句,因此它失败了。因此,案例 2 查询的这个实例完全失败(通过 writes 回溯,不会在回溯中重新执行)。这是上面 (C) 中的递归查询。
(E) 由于案例 2 在 (C) 的递归调用中失败,Prolog 继续回溯并通过执行第三个子句重新尝试,并将 min(E, [F|L]) 与 min(F, [1]) 统一(注意:这些是“不同的”F)将第一个 F 与 1 统一,L 与 [] 和 E 与第二个 F 统一(但未实例化 - 未分配值)。这里需要注意的是,在 Prolog 中,两个变量可以统一但尚未赋值。由于第三条的头部已经统一,case 3 执行,我们看到:
case 3: _L164 [1] % This is E (uninstantiated) and [F|L] ([1|[]])
_L164 出现是因为我们正在编写一个未实例化的变量。在这样的输出中,未实例化的变量显示为生成的变量名称,前面带有下划线 (_)。
(F) 所以案例 3 执行并对 min(E, L) 进行递归调用,其中 E 未实例化,L 是 []。此查询将失败,因为没有匹配 min(_, []) 的子句。然后 Prolog 将从案例 3 回溯,然后从 (C) 到 min(F, [1]) 的整个递归调用失败。
(G) 请记住,在 (C) 中描述的情况 2 中,我们从递归调用到达 (F)。由于该递归调用失败(如 (D) 到 (F) 中所述),Prolog 通过回溯、使案例 2 失败并转到案例 3 来恢复 (C) 中描述的案例 2。谓词的整个执行来自原始查询min(E, [2,1])。第三个子句的开头是min(E, [F|L]),Prolog 将第一个E 与第二个E 统一起来(但未实例化),将F 与2 统一起来,将L 与[1] 统一起来。我们现在看到:
case 3: _G323 [2,1] % This is E (uninstantiated) and [F|L] ([2|[1]])
(H) 案例 3 继续并在 min(E, [1]) 上进行递归查询(已用 [1] 实例化 L)再次从顶部开始,匹配第一个子句 min(E, [E]) 和序言将 E 与1 并匹配子句的头部。然后我们看到:
case 1: 1
(I) 案例 1 成功,然后返回案例 3,继续检查 E =< F 是否为 1 =< 2(参见 (G) 中的统一),这是正确的。我们现在已经完全成功案例 3!
我们完成了!随着案例 3 的成功(案例 1 如 (A) 所述失败,案例 2 如 (E) 所述失败),原始查询通过将 E 与 1 统一成功,我们看到:
E = 1.
当您在 Prolog 中进行查询时,它将从您正在查询的谓词的第一个子句开始,并按顺序尝试每个子句,直到找到一个成功的子句,然后它将声明成功。如果它们都失败了,那么查询当然会失败。在尝试每个子句的过程中,如果有递归查询(调用),则该递归调用将再次从第一个子句开始。每个递归调用都是对谓词的完整查询。因此,每个递归调用都将从谓词的第一个子句开始,通过谓词的每个子句进行自己的求真之旅。这是了解 Prolog 的一个重要原则,有助于理解基本的递归行为。
关于跟踪的话题,代码中的write 语句很好地显示了谓词触发的子句。但是它们没有显示子句中的哪些查询失败,这在尝试了解查询中发生的情况时同样重要。因此,仅使用 write 语句可能会有些混乱。 @User 建议的gtrace(或trace)命令将显示成功和失败的查询。这是一个很好的工具,可以用来查看子句中发生了什么,也许可以与write 语句一起查看变量等。