如何更优雅地编写这个DCG?

How to write this DCG more elegantly?

在玩 DCG 时遇到了以下问题:

我想解析并生成精确数量的空格 (" ")。我简单地这样做的简单方法:

trivial_nat_space(0) --> [].
trivial_nat_space(N) -->
    { N > 0, N is M+1 },
    " ",
    trivial_nat_space(M).

非常失败,因为 NM 的实例化不充分,具体取决于我是否这样做

?- String="   ", my_string_codes(String,Codes), phrase(trivial_nat_space(Anz_Spaces), Codes, [])

?- Anz_Spaces=3,my_string_codes(String,Codes), phrase(trivial_nat_space(Anz_Spaces), Codes, [])

其中(为方便起见)

my_string_codes(S,C) :-
    when((ground(S);ground(C)), string_codes(S,C)).

为了寻找解决问题的好方法,我制作了一个依赖于自定义 nats 的版本:

z.
s(z).
s(s(O)) :-
  s(O).

nat_num(S,C) :-
    when((ground(S);ground(C)),nat_num_(S,C)).
nat_num_(z,0) :- !.
nat_num_(s(N),X) :-
    nonvar(X),
    !,
    X > 0,
    Y is X-1,
    nat_num_(N,Y).
nat_num_(s(N),X) :-
    var(X),
    nat_num_(N,Y),
    X is Y+1.

n_space(z) --> [].
n_space(s(N)) -->
    " ",
    n_space(N).

我想避免这种情况,因为内置数字中已经存在自然数的附加编码。

还有这个:

nat_space(0) --> [].
nat_space(N) -->
    { var(N) },
    " ",
    nat_space(M),
    { N is M+1 }.
nat_space(M) -->
    { nonvar(M), M>0 },
    " ",
    { N is M-1 },
    nat_space(N).

哪个工作正常:

?- Anz_Spaces=3,my_string_codes(String,Codes), once(phrase(nat_space(Anz_Spaces), Codes, [])).
Anz_Spaces = 3,
String = "   ",
Codes = [32, 32, 32].

?- String="   ",my_string_codes(String,Codes), once(phrase(nat_space(Anz_Spaces), Codes, [])).
String = "   ",
Codes = [32, 32, 32],
Anz_Spaces = 3.

但是 nat_spaces 的编码(在我看来)远非如此:它依赖于元谓词来强制执行特定的执行顺序,并且(更严重的是):如果解析器比只是 " ",逻辑必须在单独的 DCG predicate/rule 中定义,导致单个 parser/generator 的逻辑被分成两个定义(分开的一个和一个执行正确的执行顺序)。

这是解决此类问题的 canonical/standard 方法还是我现在缺少更通用、更优雅的解决方案?

附加信息:

我正在为 x86_64-linux 使用 SWI-Prolog 版本 8.3.9 使用 :- [library(dcg/basics)] 并且在启动运行时时没有其他参数。我也不使用定义在文件中设置任何设置。

对于您的具体示例,您可以使用 CLP(fd) 以能够以两种方式使用 DCG:

trivial_nat_space(0) --> [].
trivial_nat_space(N) -->
    { N #> 0, M #= N-1 },
    " ",
    trivial_nat_space(M).

在以下示例运行中,我将使用反引号 (`) 来使用编码字符串:

?- phrase(trivial_nat_space(Anz_Spaces), `   `, []).
Anz_Spaces = 3 ;
false.

?- phrase(trivial_nat_space(3), Spaces, []).
Spaces = [32, 32, 32] ;
false.

?- phrase(trivial_nat_space(N), Spaces, []).
N = 0,
Spaces = [] ;
N = 1,
Spaces = [32] ;
N = 2,
Spaces = [32, 32] ;
N = 3,
Spaces = [32, 32, 32] 
 ...

坦率地说,您最初的定义并没有非常失败。不,它不会失败。对于最一般的查询,它会生成一个解决方案,

?- phrase(trivial_nat_space(0), Cs).
   Cs = []                                    % pure perfect logic!
;  false.
?- phrase(trivial_nat_space(-1), Cs).
false.                                        % it's right to be false!
?- phrase(trivial_nat_space(1), Cs).
caught: error(instantiation_error,(is)/2)     % er...
?- phrase(trivial_nat_space(N), Cs).          % most general query
   Cs = [], N = 0                             % again, pure logic 
;  caught: error(instantiation_error,(is)/2)  % mmh...

... 否则会出现实例化错误。实例化错误并不是可能发生的最糟糕的情况。他们清楚而诚实地声明,在我们继续之前,必须提供更多信息(=实例化)。这 比假装一切都很好 要好得多。将询问更多信息的职员想象成产生实例化错误。然后将其与仅使用一些大胆的默认假设填写 IRS 表格的表格进行比较1.

为了定位实例化错误的原因,我们将使用 。因此,我将加入一些 false 目标以及一个额外的实例化以使其更容易:

trivial_nat_space(0) --> [], {false}.
trivial_nat_space(N) --> {N = 1},
    { N > 0, N is M+1, false },
    " ",
    trivial_nat_space(M).

?- phrase(trivial_nat_space(1), Cs).
caught: error(instantiation_error,(is)/2)

这是一个非常不正常的程序!而且它仍然会产生实例化错误。为了修复您的原始程序,我们必须修改剩余可见部分的某些内容。在 N is M+1 中,只有 M 会导致该错误。事实上,它是第一次在这里发生。我们可以将其替换为 M is N-1 以改进您的程序:

?- phrase(trivial_nat_space(1), Cs).
   Cs = " "                                % see section double quotes
;  false.
?- phrase(trivial_nat_space(2), Cs).
   Cs = "  "
;  false.
?- phrase(trivial_nat_space(N), Cs).
   Cs = [], N = 0
;  caught: error(instantiation_error,(is)/2) % still ...
?- phrase(trivial_nat_space(N), " ").
caught: error(instantiation_error,(is)/2)

我们的程序现在至少在知道具体的空格数时才能运行。更好的是,我们还可以使用算术表达式!

?- phrase(trivial_nat_space(4*1), Cs).    % let's indent by four spaces
   Cs = "    "
;  false.
?- phrase(trivial_nat_space(4*2), Cs).    % ... twice ...
   Cs = "        "
;  false.
?- phrase(trivial_nat_space(4*0), Cs).    % ... none?
false.
?- phrase(trivial_nat_space(0), Cs).
   Cs = []                                % here it works
;  false.

所以 N 可能是一个算术整数表达式,并且它按预期工作,除了 0 必须按字面说明。这并不是一个很深的问题,没有违反代数性质。但它并不优雅。让我们记住这一点。

回到实例化错误。为了处理这些情况,我们需要一些方法来处理这个变量 N。最简单的方法是使用 library(clpz) 或其在 SWI library(clpfd) 中的前身,如建议的那样 。是的,您可以手动执行此类操作,但因此您正在重复已投入该库的工作。出于性能原因,有时它可能有意义,但它会付出高昂的(错误缠身)代价。

所以让我们看看@gusbro 的解决方案,不要忘记添加

:- use_module(library(clpz)).  % SICStus or Scryer
:- use_module(library(clpfd)). % SWI
?- phrase(trivial_nat_space(N), Cs).
   Cs = [], N = 0
;  Cs = " ", N = 1                     % finally, logic!
;  Cs = "  ", N = 2
;  Cs = "   ", N = 3
;  ...
?- phrase(trivial_nat_space(N), "  ").
   N = 2
;  false.
?- N = 1+1, phrase(trivial_nat_space(N), "  ").
   N = 1+1                               % nice like is/2
;  false.
?-          phrase(trivial_nat_space(N), "  "), N = 1+1.
false.                                   % and out, why?

直到上次查询为止,一切都很好。因此,带有算术表达式的扩展效果不是很好。实际上它归结为以下问题:

?- N = 1+1, N #= 2.
   N = 1+1.
?-          N #= 2, N = 1+1.
false.

在第一个查询中,我们求解整数方程 1+1 #= 2 成功,在第二个查询中,我们求解 N #= 2 成功 N = 2 然后我们尝试解决失败的2 = 1+1

换句话说,对一般算术表达式的扩展对于约束来说效果不佳。以前,实例化错误隐藏了问题。现在我们需要以某种方式划清界限。如上所述违反交换性不是一个好的选择2.

解决方案是显式分离表达式变量和整数变量,并坚持使用完全实例化的表达式。

?- N = 1+1, #N #= 2.
caught: error(type_error(integer,1+1),must_be/2)
?-          #N #= 2, N = 1+1.
false.
?- assertz(clpz:monotonic).
   true.
?-          N #= 2, N = 1+1.
caught: error(instantiation_error,instantiation_error(unknown(_102),1))

所以现在@gusbro 的程序进行了一些细微的修改:

trivial_nat_space(0) --> [].
trivial_nat_space(N) -->
    { #N #> 0, #M #= #N-1 },
    " ",
    trivial_nat_space(M).

double_quotes

既然你想要优雅的代码, to use as a 文本:字符列表。通过这种方式,您可以避免所有这些永远不会优雅的转换代码。在 Tau、Trealla 和 Scryer 等系统中,双引号项默认为 chars。在 SWI 中像这样进行:

?- L = "ab", L = [_,_].
false.

?- phrase("ab","ab").
false.

?- set_prolog_flag(double_quotes, chars).
true.

?- L = "ab", L = [_,_].
L = [a, b].

?- phrase("ab","ab").
true.

library(double_quotes)

?- L = "ab", L = [_,_].
L = "ab".

语法

最后,关于一般的多向语法规则,还有一些需要注意的地方。想想一个谓词 text_ast/2。对于一个抽象语法树,有无限的有效程序文本,它们都因布局文本等琐碎的用具而不同。因此,当只给出 AST 时,这个一般关系 一定不会 终止。所以你需要一个额外的参数来指示文本是否应该是规范的。



1 事实上,在 DEC10 Prolog 中,算术表达式中变量的默认假设是零值。 ISO Prolog 为这些情况定义了实例化错误。


2 在 SICStus 的原生库 clpfd 中,同样的问题出现在 ?- N = 1+1, call(N #= 2). 中。

在这种情况下,我们可以完全避免显式算术,让统一来完成工作:

:- set_prolog_flag(double_quotes, chars).

spaces --> "".
spaces --> " ", spaces.

n_spaces(N, Spaces) :-
    length(Spaces, N),
    phrase(spaces, Spaces).
?- n_spaces(N, S).
N = 0,
S = [] ;
N = 1,
S = [' '] ;
N = 2,
S = [' ', ' '] 

?- n_spaces(2, S).
S = [' ', ' '].

?- n_spaces(N, "  ").
N = 2.

我们在这里需要 double_quotes 标志,因为(至少在 SWI-Prolog 8.0.2 中)似乎字符串必须是 ground 或 var,不像列表可以有变量 entries/tails,所以我认为不可能将这种统一技术与 SWI-style 字符串一起使用:

?- string_length(String, Length).
ERROR: Arguments are not sufficiently instantiated
ERROR: In:
ERROR:    [8] string_length(_10886,_10888)
ERROR:    [7] <user>