应该编写文本处理 DCG 来处理代码还是字符?或两者?

Should text-processing DCGs be written to handle codes or chars? Or both?

在 Prolog 中,传统上有两种表示字符序列的方法:

DCG 是一种编写从左到右列表处理代码的符号方式,旨在对“分解文本列表”执行解析。根据偏好,要处理的列表可以是字符列表或代码列表。但是,char/code 处理的符号在写下常量时有所不同。通常是用“字符风格”还是“代码风格”来编写 DCG?或者在模块导出 DCG 非终端的情况下,甚至可能采用 char/code 样式以实现可移植性?

一些研究

以下符号可用于表示 DCG 中的常量

例子

字符样式

尝试识别“字符样式”中的“任何数字”并使其“字符表示”在C中可用:

zero(C) --> [C],{C = '0'}. 

nonzero(C) --> [C],{member(C,['1','2','3','4','5','6','7','8','9'])}.

any_digit(C) --> zero(C).
any_digit(C) --> nonzero(C).

代码风格

正在尝试识别“代码风格”中的“任意数字”:

zero(C) --> [C],{C = 0'0}.

nonzero(C) --> [C],{member(C,[0'1,0'2,0'3,0'4,0'5,0'6,0'7,0'8,0'9])}.

any_digit(C) --> zero(C).
any_digit(C) --> nonzero(C).

Char/Code透明样式

DCG 可以通过复制涉及常量的规则写成“char/code 透明样式”。在上面的例子中:

zero(C) --> [C],{C = '0'}. 
zero(C) --> [C],{C = 0'0}.

nonzero(C) --> [C],{member(C,['1','2','3','4','5','6','7','8','9'])}.
nonzero(C) --> [C],{member(C,[0'1,0'2,0'3,0'4,0'5,0'6,0'7,0'8,0'9])}.

any_digit(C) --> zero(C).
any_digit(C) --> nonzero(C).

以上还接受一系列交替的代码和字符(因为无法输入内容列表)。这可能不是问题)。当生成时,会得到任意char/code不需要的混合,然后需要添加剪切。

Char/Code 透明样式采用额外的 Mode 指标

另一种方法是明确指示模式。看起来干净:

zero(C,chars) --> [C],{C = '0'}. 
zero(C,codes) --> [C],{C = 0'0}.

nonzero(C,chars) --> [C],{member(C,['1','2','3','4','5','6','7','8','9'])}.
nonzero(C,codes) --> [C],{member(C,[0'1,0'2,0'3,0'4,0'5,0'6,0'7,0'8,0'9])}.

any_digit(C,Mode) --> zero(C,Mode).
any_digit(C,Mode) --> nonzero(C,Mode).

Char/Code 使用方言特征的透明样式

或者,可以使用 Prolog 方言的特性来实现 char/code 透明度。在 SWI-Prolog 中,有 code_type/2,它实际上适用于代码和字符(有一个相应的 char_type/2,但恕我直言,无论如何应该只有 chary_type/2 适用于字符和代码)对于“digit-class”代码和字符产生化合物 digit(X):

?- code_type(0'9,digit(X)).
X = 9.

?- code_type('9',digit(X)).
X = 9.

?- findall(W,code_type('9',W),B).
B = [alnum,csym,prolog_identifier_continue,ascii,
     digit,graph,to_lower(57),to_upper(57),
     digit(9),xdigit(9)].

因此可以为了干净 char/code 透明度而编写此代码:

zero(C) --> [C],{code_type(C,digit(0)}. 

nonzero(C) --> [C],{code_type(C,digit(X),X>0}.

any_digit(C) --> zero(C).
any_digit(C) --> nonzero(C).

特别是在 SWI-Prolog 中

SWI-Prolog 默认首选 codes。试试这个:

旗帜

影响“标准代码”中"string"`string`的解释。默认情况下,"string" 被解释为原子“字符串”,而 `string` 被解释为“代码列表”。

在 DCG 之外,以下内容适用于 SWI-Prolog,所有标志均为默认值:

?- string("foo"),\+atom("foo"),\+is_list("foo").
true.

?- L=`foo`.
L = [102,111,111].

但是,在 DCG 中,"string"`string` 默认都被解释为“代码”。

不改变任何设置,考虑这个 DCG:

representation(double_quotes)    --> "bar".            % SWI-Prolog decomposes this into CODES 
representation(back_quotes)      --> `bar`.            % SWI-Prolog decomposes this into CODES
representation(explicit_codes_1) --> [98,97,114].      % explicit CODES (as obtained via atom_codes(bar,Codes))
representation(explicit_codes_2) --> [0'b,0'a,0'r].    % explicit CODES 
representation(explicit_chars)   --> ['b','a','r'].    % explicit CHARS

以上哪个符合代码?

?- 
findall(X,
   (atom_codes(bar,Codes),
    phrase(representation(X),Codes,[])),
   Reps).

Reps = [double_quotes,back_quotes,explicit_codes_1,explicit_codes_2].

以上哪个匹配字符?

?- findall(X,
   (atom_chars(bar,Chars),phrase(representation(X),Chars,[])),
   Reps).
Reps = [explicit_chars].

当使用 swipl --traditional 启动 swipl 时,反引号表示被 Syntax error: Operator expected 拒绝,但除此之外没有任何变化。

Prolog 标准 (6.3.7) 说:

A double quoted list is either an atom (6.3.1.3) or a list (6.3.5).

因此,以下应该会成功:

Welcome to SWI-Prolog (threaded, 64 bits, version 7.6.4)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

For online help and background, visit http://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

?- Foo = "foo", (atom(Foo) ; Foo = [F, O, O]).
false.

所以 SWI-Prolog 默认不是 Prolog。没关系,但如果您想了解 SWI-Prolog 的非 Prolog 行为,请调整问题上的标签。

根据定义,双引号列表 默认情况下是完全无用的 即使在一致的 Prolog 中也是如此:它们可能表示原子,因此无论 chars/codes区别你甚至不知道双引号列表实际上是一个列表。如果“列表”实际上是一个原子,即使 DCG 只关心“文本”的结构属性(例如它是否是回文)也是无用的。

因此,想要使用 DCG 处理文本的 Prolog 程序必须 在启动时强制 double_quotes 标志为它想要的值。您可以在代码和字符之间进行选择。代码与字符相比没有优势,但在可读性和可键入性方面确实存在劣势。因此:

答:使用字符。明确设置 double_quotes 标志。

我应该首先注意到 “文本处理 DCG 应该被编写来处理代码还是字符?或者两者兼而有之?” 问题的答案可以是 都没有。 DCG 通过使用隐式差异列表来处理线程状态。但是该差异列表的元素可以不是字符或代码。这取决于文本标记化的输出以及 文本处理 究竟需要什么。例如。我从事并遇到过 Prolog NLP 应用程序,其中 codes/chars 仅用于基本标记化,并且使用原子或复合术语执行推理(仍然使用 DCGS),这些术语具体化了标记数据(例如 v(Verb)n(Noun))。其中一个应用程序(个人助理,就像现在手机中常见的那样)使用了语音识别组件产生的原子。

但让我们回到 charscodes。遗留实践和失败的标准化给 Prolog 留下了有问题的文本表示。 ASCII 为我们提供了单引号、反引号和双引号。对于原子使用单引号,可以选择使用例如表示代码列表的反引号和表示字符列表的双引号。或者反过来。相反,这就是标准化失败的地方,我们得到了有问题的 double_quotes 标志。不乏对双引号术语的含义做出假设的 Prolog 代码,因此工作或中断取决于 double_quotes 标志的隐式值(如果您认为这主要是遗留问题代码,再想一想)。猜猜当我们尝试组合需要不同标志值的代码时会发生什么?请注意,在几乎所有系统(包括支持模块的系统)中,标志值为 global ... 正如 Isabelle 在她的回答中所写,明确设置标志是很好的一般建议。但正如我所解释的,并非没有问题。

一些系统为标志提供额外的值。例如。 SWI-Prolog 也允许将标志设置为 string。 GNU Prolog 支持额外的 atom_no_escapechars_no_escapecodes_no_escape。有些系统只支持codes。一些系统还提供了一个 back_quotes 标志。这个巴别塔意味着可移植弹性代码经常被迫使用原子来表示文本。但从性能角度来看,这可能并不理想。

回到最初的问题。正如 Isabelle 提到的,chars 通常是一个更具可读性(阅读,更容易调试)的选择。但是,根据 Prolog 系统,codes 可能会提供更好的性能。如果应用程序性能至关重要,请对这两种解决方案进行基准测试。一些最近的 Prolog 系统(例如 Scryer-Prolog 或 Trealla Prolog)对 chars 有有效的支持。较旧的系统可能落后。

您的假设不正确。这些不是“字符”:

foo_or_bar(foo) --> "foo".

"foo" 在 SWI-Prolog 中是一个字符串,但这在 DCG 规则定义 中非常有效。阅读此内容的地方是 here,特别是:

A DCG literal

Although represented as a list of codes is the correct representation for handling in DCGs, the DCG translator can recognise the literal and convert it to the proper representation. Such code need not be modified.

您的所有其他建议都是不必要的,您应该明确枚举所有可能的“非零”、数字等,或者使用库。

PS:如果您的主要目标是编写可在任何 Prolog 上运行的代码,您不妨改用 Logtalk 之类的东西。

请注意,您的问题与 I/O 密切相关。在 ISO 之前,DEC-10 继承中的许多系统通过 get0/1put/1(以及 tty 的版本)支持一种 I/O,它同时提供字符和字节同时。那有什么问题呢?今天,这是显而易见的。但是多八位字节字符集处理(MOCSH 因为它被称为)对于许多人来说是一个更加奇特的功能,就像今天,在标准发布四分之一个世纪之后。毕竟,今天最被接受的 UTF-8 编码是 发明的 1992-09 并于 1993 年首次发布。就像 TRON 这样的许多项目一样,它也可能失败了。一些其他编程语言因押注 UCS-2/UTF-16 编码而被烧毁。

标准所做的是将I/O拆分为字符和字节I/O(以及它们对应的类型textbinary)。所以现在有 get_char/1get_byte/1 ... _byte 版本都使用 0..255 范围内的整数是没有争议的(加上 EOF 的 -1)。但是 _char 版本呢?解决这个问题的唯一方法是同时提供 _char_code 版本,并因此提供 charscodes 版本的双引号字符串和相关内置函数。标志 double_quotes 的默认值是实现定义的 (7.11.2.5)。

以这种方式,具有大量 DEC-10 遗产的系统可以继续明确地使用代码。因此,对于他们来说,一个整数意味着一个整数或一个字节或一个字符。但是这种系统的用户仍然可以使用更好的编码。不必处理这些可追溯到 1977 年的遗留问题的新系统默认选择 chars,例如 Tau、Scryer 和 Trealla。就传统而言,请注意通常称为 Marseille Prolog 的 Prolog I 确实将双引号字符串编码为长度为 1 的原子列表。在 1972 年的 Prolog 初始版本(通常称为 Prolog 0)中,字符串被编码为 nil-s-t-r-i-n-g qua boum 以促进词干提取。无论如何,一个字符代码都没有。

chars的优势应该是显而易见的。它更容易阅读和调试,特别是如果您有部分实例化的字符串,比如 [a,X,c][97,X,99],这在使用 library(diadem). It is also a bit shorter to write. And, double quoted strings can be used for printing answers.[=47= 概括查询时经常发生]

如果你真的想编写同时支持codeschars的程序,那么使用像[Ch] = "a"这样的目标其中 Ch 现在是原子 a 或整数 97 或 129 或您正在使用的任何处理器字符集。这完全取决于 Prolog 标志 double_quotes。你可以更简洁地写

nonzero(C) --> [C],{member(C,"123456789")}.

更重要的是phrase("abc", "abc")仍然成立。

但是,在同一个应用程序中更改该标志肯定不是一个好主意(也不是切换到值 atom 或一些不合格的值)。

((当使用 chars 时,请注意 C = 'a' 中的单引号有点误导,因为单引号没有任何作用。相反,如果您想确保即使存在 a 的运算符声明,代码也将有效。当 a 作为仿函数的参数或列表的元素出现时,不需要圆括号,但它们经常在运算符声明。))