也许这个例子有帮助。我暂时不考虑半上下文技巧,作为稍后的讨论。
我们想计算一个原子中出现和a
出现的次数。任何其他字符都归为一个类别(但也算在内)。b
c
dropped
因此,“状态”是a
, b
, c
,的当前计数dropped
。状态应使用映射来实现,在这种情况下是 SWI-Prolog 字典。
三种方法:
- 和
foldl/4
- 和
phrase/3
- 使用原版 Prolog
使用以下代码:
?-
count_with_foldl(abgdbabba,DictWithCounts).
DictWithCounts = counts{a:3,b:4,dropped:2}.
?-
count_with_phrase(abgdbabba,DictWithCounts).
DictWithCounts = counts{a:3,b:4,dropped:2} ;
false.
?-
count_with_recursion(abgdbabba,DictWithCounts).
DictWithCounts = counts{a:3,b:4,dropped:2}.
代码:
% ===
% Morph DictIn to DictOut so that:
% Only for Keys [a,b,c]:
% If Key exists in DictIn, DictOut is DictIn with the count for Key incremented
% If Key notexists in DictIn, DictOut is DictIn with a new entry Key with count 1
% ===
inc_for_key(Key,DictIn,DictOut) :-
memberchk(Key,[a,b,c]),
!,
add_it(Key,DictIn,DictOut).
inc_for_key(Key,DictIn,DictOut) :-
\+memberchk(Key,[a,b,c]),
add_it(dropped,DictIn,DictOut).
add_it(Key,DictIn,DictOut) :-
(get_dict(Key,DictIn,Count) -> succ(Count,CountNew) ; CountNew=1),
put_dict(Key,DictIn,CountNew,DictOut).
% ===
% Using foldl to count
% ===
count_with_foldl(Atom,DictWithCounts) :-
atom_chars(Atom,Chars),
foldl(inc_for_key,Chars,counts{},DictWithCounts).
% ===
% Using a DCG to count
% ===
dcg_count(Dict,Dict) --> [].
dcg_count(DictIn,DictOut) --> [C], { inc_for_key(C,DictIn,Dict2) }, dcg_count(Dict2,DictOut).
count_with_phrase(Atom,DictWithCounts) :-
atom_chars(Atom,Chars),
phrase(dcg_count(counts{},DictWithCounts),Chars).
% ===
% Using standard Prolog to count
% ===
count_with_recursion(Atom,DictWithCounts) :-
atom_chars(Atom,Chars),
count_rec(Chars,counts{},DictWithCounts).
count_rec([],Dict,Dict).
count_rec([C|Cs],DictIn,DictOut) :- inc_for_key(C,DictIn,Dict2), count_rec(Cs,Dict2,DictOut).
通过plunit
测试:
:- begin_tests(counting).
test("count using foldl/4, #1", true(DictWithCounts == counts{a:3,b:4,dropped:2})) :-
count_with_foldl(abgdbabba,DictWithCounts).
test("count whith phrase/2, #1", [true(DictWithCounts == counts{a:3,b:4,dropped:2}),nondet]) :-
count_with_phrase(abgdbabba,DictWithCounts).
test("count whith recursion, #1", [true(DictWithCounts == counts{a:3,b:4,dropped:2})]) :-
count_with_recursion(abgdbabba,DictWithCounts).
:- end_tests(counting).
DCG 中的单个状态变量
在上面,DCG 在参数位置 1 处采用“输入状态”,它们发展成“输出状态”,最终在参数位置 2 处返回。
dcg_count(DictIn,DictOut)
这完全类似于函数式编程,其中不可变结构被传递给函数并由函数返回。在这里,结构通过谓词“相关”,这是另一种看待事物的方式(这种方式有时会与必须及时向前计算的机器的现实相冲突)。
请注意,上面的状态变量是基本的 - 没有变量。
如果该状态变量是较大结构的叶子上的“空单元”的名称,则单个状态变量就足够了。较大的结构“在它的叶子上生长”,一个 DCG 填充空单元格,但为下一次调用留下另一个空单元格。
例如,这是一个 DCG,它在其末尾增长了一个“开放列表” Fin
。Fin
始终是应由规则填充的未绑定变量:
在原子中替换tag
为:
:
collect(Fin) --> [], { Fin = [] }.
collect(Fin) --> [t,a,g], !, collect(Fin2), { Fin = [':'|Fin2] }.
collect(Fin) --> [C], collect(Fin2), { Fin = [C|Fin2] }.
collect(Atom,Collected) :-
atom_chars(Atom,Chars),
phrase(collect(Collected),Chars).
?- collect(xytagyx,Out).
Out = [x,y,:,y,x] ;
false.
传统上,DCG 代码编写得更紧凑(这种方式也使其能够进行尾调用优化,这一点不容忽视):
collect([]) --> [].
collect([':'|Fin]) --> [t,a,g], !, collect(Fin).
collect([C|Fin]) --> [C], collect(Fin).
collect(Atom,Collected) :-
atom_chars(Atom,Chars),
phrase(collect(Collected),Chars).
在这种情况下,每个 DCG 调用只能看到打开列表远端的空单元格,而Collectecd
表示它的开始:
Collected ----> [|]
/ \
x [|]
/ \
: ~empty cell~ <--- Fin
所以,当遇到y
规则
collect([C|Fin]) --> [C], collect(Fin).
只是Fin
尽其所能,将列表增加 1 个列表单元,但将其保留为“打开列表”以继续附加:
Collected ----> [|]
/ \
x [|]
/ \
: [|]
/ \
C ----> y ~empty cell~ <--- Fin
规则
collect(Fin) --> [], { Fin = [] }.
将打开的列表转换为正确的列表,将空单元格设置为[]
:
Collected ----> [|]
/ \
x [|]
/ \
: [|]
/ \
y []
这当然正是DCG Primer的树示例中发生的情况:
tree_nodes(nil) --> [].
tree_nodes(node(Name, Left, Right)) -->
tree_nodes(Left),
[Name],
tree_nodes(Right).
在这里,在它的叶子上增长的数据结构不是一个列表,而是一个二叉树。