跳转至内容

Rebol 编程/语言特性/解析/解析表达式

来自 Wikibooks,开放书籍,开放世界

有时您需要解析一个序列,以查看它是否与特定的格式匹配。这可用于简单的事情,例如确定和验证电话号码或电子邮件地址的格式。

即使在拆分字符串时,您也可能需要此功能,但不想将引号专门视为 NONE 或字符串规则所做的那样。(参见简单拆分示例。)

PARSE 不使用正则表达式,而是匹配解析表达式解析表达式

  • 是 Rebol 的方言(解析方言)。
  • 是自上而下解析语言(TDPL)家族中(增强过的)成员,包括自上而下解析语言(TDPL)、广义自上而下解析语言(GTDPL)和解析表达式语法(PEG)。
  • 使用与 TDPL 家族中其他成员相同的“有序选择”解析方法。
  • 通过有序选择运算符,具有无限的预读能力。
  • 与 TDPL 家族中其他成员兼容。
  • 可以很好地替代正则表达式,因为它们的功能更强大。例如,正则表达式本质上不能找到匹配的括号对,因为它不是递归的,而 PARSE 方言可以。
  • 的功能比上下文无关语法强大得多。
  • 上下文无关语法通常需要一个单独的词法分析步骤,因为它们使用预读的方式。PARSE 对字符串类型的词法分析不需要单独的词法分析步骤,并且词法分析规则可以像其他语法规则一样编写。
  • 许多上下文无关语法包含固有的歧义,即使它们旨在描述无歧义的语言。C、C++ 和 Java 中的“悬空 else”问题就是一个例子。这些问题通常通过在语法之外应用规则来解决。在 PARSE 方言中,由于优先级,这些歧义永远不会出现。
  • 可以很好地替代 LL 解析器,因为它们的功能更强大。


PARSE 处理整个语言中使用的强大方言。例如,VID视觉界面方言用于构建图形用户界面。

解析表达式匹配

[编辑 | 编辑源代码]

PARSE 在解析表达式匹配期间所做的事情是遍历一个序列(例如块、字符串或二进制),并在此过程中,您可以执行操作或从序列中收集信息,以在其他地方使用或对序列本身执行操作。

解析表达式匹配的用法主要有两种方式:一种是对字符串匹配字符模式,另一种是对块匹配 Rebol 值的模式。这意味着

块解析通常用于处理方言,这是该语言的主要特征之一。

对于解析表达式匹配,给定的 RULE 参数必须是一个块。块的内容被解释为起始解析表达式,对应于解析表达式语法中的起始表达式

在匹配解析表达式时,PARSE 会维护输入位置

解析表达式匹配可能会有两种结果

  • 成功,在这种情况下,PARSE 可能会选择向前移动输入位置,或者
  • 失败,在这种情况下,输入位置保持不变

如果满足以下两个条件,则 PARSE 返回 TRUE:

  • 起始解析表达式被发现成功地匹配给定的 INPUT
  • 最终的输入位置位于给定 INPUT 的尾部

否则,PARSE 返回 FALSE。

原子解析表达式

[编辑 | 编辑源代码]

原子解析表达式是只由一个 Rebol 值表示的解析表达式

NONE 被视为一个非终结符,它可以成功地匹配任何输入,并且通常不会向前移动输入位置,在字符集部分提到了一个例外。

 parse "" [#[none]]
 ; == true

这将返回 TRUE,因为

  • NONE 值可以成功地匹配任何输入。
  • PARSE 已经位于字符串的尾部。
parse [] [#[none]]
; == true

注意

在解析字符串以及解析块时,字符都被视为终结符

parse "a" [#"a"]
; == true

这将返回 TRUE,因为

  • #"a" 字符可以成功地匹配当前输入位置的字符。
  • PARSE 在成功匹配后向前移动,在这种情况下,它到达了输入的末尾。
parse [#"a"] [#"a"]
; == true

任何字符串

[编辑 | 编辑源代码]

解析字符串时,任何字符串都被视为终端符号序列

parse "aaa" ["aaa"]
; == true

这将返回 TRUE,因为

  • 该字符串成功匹配了当前输入位置处的输入的一部分。
  • PARSE 在成功匹配后向前移动,在这种情况下,它到达了输入的末尾。
parse "<html>" [<html>]
; == true

解析块时,任何字符串都被视为终端符号

parse ["aaa"] ["aaa"]
; == true
parse [<html>] [<html>]
; == true

块被视为非终结符;它们的内容被解释为解析表达式并用于匹配。

parse "a" [["a"]]
; == true
parse ["a"] [["a"]]
; == true

注意

  • 由于 Rebol 块被视为非终结符,因此它们不能用作终端符号来字面上匹配输入中包含的特定块。要匹配特定块,您可以使用into 运算符或解析习语 部分中定义的quote 习语。

括号成功匹配任何不前进 PARSE 位置的输入;它们被视为动作以供评估,这会导致在下面的示例中将字符串“OK”打印到控制台

parse "" [(print "OK")]
; == true
parse [] [(print "OK")]
; == true

注意

  • 由于括号被视为动作,因此它们不能用作终端 来匹配输入块中的括号。如果要匹配特定括号,可以使用into 运算符或解析习语 部分中的quote 习语。

无参数 PARSE 关键字

[编辑 | 编辑源代码]

不需要任何参数的 PARSE 关键字被视为非终结符.

end 关键字

[编辑 | 编辑源代码]
parse "" [end]
; == true

这将返回 TRUE,因为

  • 输入已在尾部。
  • 当输入位于尾部时,end 关键字成功匹配输入。

skip 关键字

[编辑 | 编辑源代码]
parse "a" [skip]
; == true

这将返回 TRUE,因为

  • skip 关键字成功匹配任何终端.
  • PARSE 在成功匹配后向前移动,在这种情况下,它到达了输入的末尾。
parse ["aa"] [skip]
; == true

注意

  • PARSE 关键字不能用作终端 来匹配输入块中的特定单词。有关匹配此类单词的信息,请参见Lit-word 部分。

不是 PARSE 关键字的 Rebol 单词被视为非终结符。查找此类单词的值并将其用于匹配。

parse "" [none]
; == true

这将返回 TRUE,因为

  • 'none 变量引用 NONE 值,该值成功匹配任何输入。
  • PARSE 已经位于输入的末尾。

注意

  • 有关在输入中匹配特定单词的信息,请参见Lit-word 部分。
  • 引用 PARSE 关键字的单词按关键字处理。
  • 不支持引用其他单词的单词。当 PARSE 遇到此类单词时,它会导致“无效参数”错误。

Lit-word 只能在块解析期间使用。每个 lit-word 被视为非终结符,它成功匹配当前输入位置处的对应单词。

parse [Hi] ['Hi]
; == true

不同的单词匹配将失败

parse [Bye] ['Hi]
; == false

注意

  • 由于 lit-word 是非终结符,因此它们不能用作终端 来匹配输入块中的 lit-word。有关匹配特定 lit-word 的信息,请参见解析习语 部分中的quote 习语。

路径与单词类似,即查找路径的值并将其用于匹配。

Lit-path 与 lit-word 类似,即它们匹配输入块中的对应路径。

注意

  • 由于 lit-path 是非终结符,因此它们不能用作终端 来匹配输入块中的 lit-path。有关匹配特定 lit-path 的信息,请参见解析习语 部分中的quote 习语。

字符集

[编辑 | 编辑源代码]

解析字符串时,位集作为字符集 非终结符。它们成功匹配它们包含的任何字符。

whitespace: charset [#"^A" - #" " #"^(7F)" #"^(A0)"]
parse/all " " [whitespace]
; == true

请注意,我们通过使用 /ALL 精炼“关闭”了空格字符的特殊处理。

了解一下如果我们不“关闭”空格字符的特殊处理会发生什么,可能很有趣

whitespace: charset [#"^A" - #" " #"^(7F)" #"^(A0)"]
parse " " [whitespace]
; == false

结果为 FALSE,因为 PARSE 在这种情况下“忽略”空格字符,因此无法成功匹配它们。

下一个尝试也不成功

parse " " []
; == false

,因为输入位置尚未位于尾部。

要成功,我们需要执行

parse " " [none]
; == true

,其中 NONE 值用于匹配空格并向前移动 PARSE 输入位置。

注意

  • 上述 NONE 行为是一个特例,即使 NONE 值也可以向前移动当前输入位置。
  • 在块解析的情况下,位集的行为像终端符号.

数据类型

[编辑 | 编辑源代码]

解析块时,我们可以使用 Rebol 数据类型作为非终结符。它们成功匹配任何对应数据类型的值。

parse [5] [integer!]
== true

这将返回 TRUE,因为

  • 'integer! 单词引用的 INTEGER! 数据类型成功匹配了块中的元素。
  • PARSE 在向前移动后到达了块的末尾。

同样的事情,只是使用日期和字符串

parse [25-Dec-2005] [date!]
; == true
parse ["Hello"] [string!]
; == true

NONE! 数据类型可以用于匹配输入中的 NONE 值

parse [#[none]] [none!]
; == true

注意

  • 由于数据类型被视为 非终结符,因此它们不能用作 终结符 来匹配输入块中的数据类型。要匹配输入中的特定数据类型,请参阅 解析习语 部分中的引号习语。
  • INTEGER! 数据类型在字符串解析期间用于匹配整数表示形式。其他数据类型“工作”为 NONE。

设置词

[edit | edit source]

设置词被视为 非终结符,用于获取当前输入位置。设置词始终成功,不会移动输入位置

parse "123" [position:]
; == false
position
; == "123"

解释

  • PARSE 将“位置变量”设置为引用当前输入位置
  • 匹配成功,不向前移动输入位置
  • PARSE 返回 FALSE,因为最终的输入位置没有到达输入的尾部

注意

  • 由于设置词被视为 非终结符,因此它们不能用作 终结符 来匹配输入块中的特定设置词。要匹配特定设置词,请参阅 解析习语 部分中的引号习语。

获取词

[edit | edit source]

获取词被视为 非终结符,用于设置当前输入位置。尝试将输入位置设置为完全不同的序列(一个与当前输入位置没有相同头部的序列)会导致错误。否则匹配成功。示例

string: tail "123"
parse head string [:string]
; == true

解释

  • 匹配成功
  • PARSE 返回 TRUE,因为在这种情况下,位置被设置为输入的尾部。

注意

  • 由于获取词被视为 非终结符,因此它们不能用作 终结符 来匹配输入块中的特定获取词。要匹配特定获取词,请参阅 解析习语 部分中的引号习语。

本地

[edit | edit source]

本地人在块解析期间匹配什么?

其他数据类型

[edit | edit source]

块解析期间可以使用除上述提到的其他数据类型之外的其他数据类型的值作为 终结符

解析操作

[edit | edit source]

让我们看看 PARSE 如何遍历一个块

parse [Hi Bye] [word!]
; == false

这里出了什么问题?发生的事情是 PARSE 成功匹配了 INPUT 块中的第一个词,并将输入推进到第二个词。

该块尚未解析到结尾,这意味着 PARSE 返回 FALSE。

为了在更复杂的情况下匹配输入,除了上面提到的原子表达式之外,我们还需要解析操作

序列

[edit | edit source]

序列操作解析表达式的序列。它不使用关键字。序列的一般形式为

subexpression_1 subexpression_2 ... subexpression_n

在匹配序列时,PARSE 匹配子表达式_1,如果成功,则尝试匹配序列的其余部分。为了使序列匹配成功,所有子表达式匹配都需要成功。

示例

parse [Hi Bye] ['Hi word!]
; == true

在这种情况下,序列匹配成功,输入被推进到其尾部,导致 PARSE 返回 TRUE。

有序选择

[edit | edit source]

有序选择(又称“备选”,但“有序选择”这个名字更合适,因为子表达式的顺序很重要)操作使用|关键字。此操作的一般形式为

subexpression_1 | subexpression_2 | ... | subexpression_n

在匹配有序选择时,PARSE 尝试匹配子表达式_1。如果成功,则有序选择匹配成功。如果第一个子表达式匹配不成功,PARSE 尝试匹配选择的其余部分。

有序选择操作的优先级低于序列操作,这意味着

e1 e2 | e3

等效于

[e1 e2] | e3

假设你想检查块元素是整数还是小数

parse [36] [integer! | decimal!]
; == true
parse [37.2] [integer! | decimal!]
; == true

注意:有序选择操作的优先级导致|中的第二个子表达式

["a" | "ab"]

选择永远不会成功,因为第一个子表达式具有优先级。

重复运算符

[edit | edit source]

重复运算符指定给定子表达式应该匹配多少次。重复的一般语法为

 repetition_operator subexpression

重复运算符的优先级高于序列运算符,这意味着

repetition_operator subexpression_1 subexpression_2

与以下含义相同

[repetition_operator subexpression_1] subexpression_2

所有重复运算符都是贪婪的,这意味着它们总是尽可能多地匹配。

重复运算符是左结合的,这意味着

any 2 skip

等效于

[any 2] skip

对于tothruinto 运算符也是如此。

零或一

[edit | edit source]

此运算符使用opt关键字。它也被称为可选匹配。由于允许零计数,因此此运算符始终成功。

示例

parse "," [opt #","]
; == true
parse "" [opt #","]
; == true

一或更多

[edit | edit source]

此运算符使用some关键字。

示例

parse "," [some #","]
; == true
parse ",," [some #","]
; == true

零或更多

[edit | edit source]

此运算符使用any关键字。由于允许零计数,因此此运算符始终成功。

示例

parse ",," [any #","]
; == true
parse "" [any #","]
; == true
parse [Hi Bye] [any word!]
; == true

它返回 TRUE,因为

  • any 运算符始终成功
  • any 运算符是贪婪的,成功匹配了输入中的所有词,将最终的输入位置留在了尾部

如果我们在块中添加不同的数据类型

parse [Hi 36 Bye] [any word!]
; == false

PARSE 返回 FALSE,因为

  • any 运算符成功匹配了输入的第一个元素,并停止,没有到达尾部

重复次数

[edit | edit source]

此操作的一般形式为

n subexpression

其中 N 是一个整数。此操作指定了给定子表达式的重复次数。

示例

parse "12" [2 skip]
; == true

该表达式检查是否有两个词,不多不少

parse [Hi Bye] [2 word!]
; == true

次数范围

[edit | edit source]

此操作的一般形式为

n m subexpression

其中 N 和 M 是整数。此操作指定了子表达式的重复范围。

示例

parse "12" [1 2 skip]
; == true
parse [Hi Bye] [1 2 word!]
; == true

该表达式检查不小于 1 个词也不大于 2 个词。

parse [Hi how are you? Bye] [0 5 word!]
; == true

此表达式将成功匹配 0 到 5 个词。

请注意,在解析整数时,我们必须指定一个范围,因为整数用于指定匹配范围。

这指定了正好一个匹配

parse [-1] [1 1 -1]
; == true

这是一个错误

parse [-1] [-1]
; == false

跳过输入中的数据

[编辑 | 编辑源代码]

有两个 PARSE 运算符根据给定的解析子表达式推进输入位置

  • to 运算符
  • thru 运算符

操作的一般语法是

to subexpression

thru subexpression

to 运算符的目的是推进输入位置直到成功子表达式匹配发生的位置。

thru 运算符的目的是将输入位置推进到成功子表达式匹配之后的位置。

如果找不到成功的子表达式匹配,则这两个操作都会失败。

子表达式可以是

  • end 关键字。该
to end
操作始终成功地将输入推进到其尾部,而该
thru end
操作在 R2 中始终失败;在更新的 R3 版本中,它已经得到改进,并与解析习语部分中提到的递归习惯用法兼容
  • 一个单词 - 在这种情况下,它的值将被查找并用于匹配

解析字符串时支持以下子表达式

  • 字符
  • 字符串

解析块时支持以下子表达式

  • 数据类型,它们将按照数据类型部分中所述进行匹配
  • 文字单词,它们将按照文字单词部分中所述进行匹配
  • 其他值将按字面意思匹配,即作为终端符号

如果您要解析大量数据并且不关心块中的某些内容,这非常有用。

假设我们不关心任何内容,直到我们找到一个单词。可以使用to 来实现这一点

parse [37.2 38 Bye] [to word!]
; == false

这使得 PARSE 返回 FALSE,因为

  • 我们已经成功找到一个单词,并且to 操作成功,但是
  • 我们还没有到达输入的尾部。

为了到达输入的尾部,我们可以使用thru 而不是to 来继续经过该单词。

parse [37.2 38 Bye] [thru word!]
; == true

子块解析

[编辑 | 编辑源代码]

解析块时,您可能需要检查其子块(或括号、路径、文字路径或获取路径)以查找特定模式。into 运算符适用于此。操作的一般形式是

into subexpression

仅接受块或引用块的单词作为子表达式。

如果当前输入位置处的元素不具有 ANY-BLOCK! 类型,则子块解析操作将失败。否则,将使用该元素(子块)作为输入,并与给定的子表达式进行匹配。为了使子块解析成功,子块必须成功匹配给定的子表达式,并且最终的子块输入位置必须是子块的尾部。

示例

parse [[]] [into [none]]
; == true
parse [[1]] [into [none]]
; == false
parse [(1)] [into [skip]]
; == true
parse [a/b] [into [2 skip]]
; == true
parse ['a/b] [into ['a 'b]]
; == true

使用输入序列中的数据

[编辑 | 编辑源代码]

您还可以使用序列中的数据在触发代码中使用。除了使用设置词获取输入位置之外,还有以下 PARSE 操作

set variable subexpression
copy variable subexpression

set 操作仅在块解析期间可用,而copy 操作在两种解析模式下都可用。如果子表达式匹配成功,则set 操作将给定变量设置为第一个匹配的值,而copy 操作则复制由给定子表达式匹配的输入的整个部分。有关更详细的说明,请参见解析习语部分。

parse [Hi 36 37.2 38 Bye] [
  word!
  any [set int integer! (print ["Integer" int "Found"]) | decimal! (print "Decimal Found")]
  word!
]
; Integer 36 Found
; Decimal Found
; Integer 38 Found
; == true

我们可以将 Series 的普通函数应用于从我们正在解析的序列中提取数据。

parse [Hi 36 37.2 38 Bye] [
  any [
    set int integer! (print ["Integer" int "Found"])
    | dec: decimal! (print ["Decimal Found at position" index? dec])
    | wrd: thru word! (print ["Word" first wrd "is near tail:" tail? wrd])
  ]
]
; Word Hi is near tail: false
; Integer 36 Found
; Decimal Found at Position 3
; Integer 38 Found
; Word Bye is near tail: true
; == true

这将复制字符串的一部分

parse "123456" [copy part 5 skip to end]
; == true
part
; == "12345"

R2 和 R3 解析之间的区别

[编辑 | 编辑源代码]

fail - R3 parse 的新关键字,一个不匹配任何输入的非终结符

quote 值 - R3 parse 的新关键字,按原样匹配该值

if(表达式) - R3 parse 的新关键字,在括号中计算表达式,如果结果是falsenone,则将其视为匹配失败

into 子规则 - 与 R2 中一样,但在 R3 中,子规则匹配的项目可以与输入具有不同的数据类型

return 值 - R3 parse 的新关键字,立即从parse 中返回给定值

and 子规则 - R3 parse 的新关键字;前瞻规则;匹配子规则但不推进输入

not 子规则 - R3 parse 的新关键字,反转匹配子规则的结果

?? - R3 parse 的新关键字,打印调试输出

then 子规则 - R3 parse 的新关键字,无论子规则匹配是否成功,都跳过下一个备选方案

any 子规则 - 为了防止 R3 中出现不必要的无限循环,此规则在子规则匹配输入但未推进时也会停止

some 子规则 - 为了防止 R3 中出现不必要的无限循环,此规则在子规则匹配输入但未推进时也会停止

while 子规则 - R3 parse 的新关键字,在子规则匹配输入时迭代子规则匹配;此规则类似于any,但具有更简单的停止条件(以循环可能变成无限循环为代价)

accept - R3 parse 的新关键字,与break 的作用完全相同

reject - R3 parse 的新关键字,停止匹配循环并指示循环匹配失败

to - 现在允许多个目标,但仍然不够通用,无法允许任何目标规则

thru - 现在允许多个目标,但仍然不够通用,无法允许任何目标规则

change 子规则only 值 - R3 parse 的新关键字,更改匹配子规则的输入部分(警告!这非常慢!此外,输入更改会损害代码的可理解性!由于这些原因,使用它不是一个好的编程实践。)

insert - R3 parse 的新关键字,(警告!这非常慢!此外,输入更改会损害代码的可理解性!由于这些原因,使用它不是一个好的编程实践。)

remove 子规则 - R3 parse 的新关键字,(警告!这非常慢!此外,输入更改会损害代码的可理解性!由于这些原因,使用它不是一个好的编程实践。)

do 子规则 - Gabriele 提出的 R3 parse 的新关键字

复杂的解析表达式

[编辑 | 编辑源代码]

可以构建复杂的解析表达式。这样做时,将它们分解成更小的部分并为它们赋予有意义的名称会很有用。

递归通常是描述语法的最优雅方式。以包含 anbn 类型字符串的语法为例,其中 n >= 1。可以使用以下解析表达式来描述这种语法

anbn: ["a" anbn "b" | "ab"]

用法

parse "ab" anbn
; == true
parse "aabb" anbn
; == true

解析习语

[编辑 | 编辑源代码]
说明 操作 习语
Any-string,字符串解析 a: ["abc"] a: [#"a" #"b" #"c"][1]
Bitset,字符串解析 a: charset ",;" a: [#"," | #";"][2]
skip 非终结符,字符串解析 a: [skip] b: 补充字符集 ""[3]
a: [b][4]
跳过 非终结符,块解析 a: [skip] a: [任何类型!][5]
可选 运算符(零或一) a: [可选 b] a: [b |][6][7]
任何 运算符(零或多个)[8] a: [任何 b] a: [b a |][9][10]
某些 运算符(一个或多个)[8] a: [某些 b] a: [b [a |]][11][12]
失败(始终失败的非终结符) a: [失败] a: [某些 "a" "a"][13]
a: [结束 跳过][14]
时间范围 运算符 a: [m n b] a: [(l: 最小 m n k: n - m) l b [k [b | c: 失败] | :c]][15][16]
然后 运算符[17]
(匹配 B 并且如果成功,匹配 C;否则匹配 D)
a: [b 然后 c | d] a: [[b (e: c) | (e: d)] e][18]
谓词[19](反转成功) a: [非 b] a: [b 然后 失败 |][20]
a: [[b (c: [失败]) | (c: 无)] c]
并且 谓词(匹配但不前进) a: [并且 b] a: [c: b :c][21]
a: [非 非 b][22]
a: [[b (c: 无) | (c: [失败])] 失败 | c][23][24]
结束 非终结符(匹配输入的尾部) a: [结束] a: [非 跳过][25][26]
开始 非终结符(匹配输入的头部)[27] a: [开始] a: [b: (c: 除非 头部? b [[失败]]) c][28][29]
运算符[8]
(前进到第一个成功匹配)
a: [到 b] a: [并且 b | 跳过 a][30][31][32]
a: [任何 [非 [b (c: 无)] (c: [失败]) 跳过] c][33]
a: [任何 [[b (c: 无 d: [失败]) | (c: [失败] d: [跳过])] d] c][34]
a: [穿过 [并且 b]][35]
任何 之间的对应关系 a: [到 [b | 结束]][36] a: [任何 [非 b 跳过]]
穿过 运算符[8]
(前进穿过第一个成功匹配)
a: [穿过 b] a: [b | 跳过 a][37][38][39]
a: [任何 [非 [b c: (d: [:c])] (d: [失败]) 跳过] d][33]
a: [任何 [[b c: (d: [:c] e: [失败]) | (d: [失败] e: [跳过])] e] d][34]
a: [到 [b c:] :c][35]
设置 运算符
(将变量设置为第一个匹配的值)
a: [设置 b c] f: [(设置/任何 [b] 如果 小于? 索引? e 索引? d [e])][40][41][42]
a: [并且 [c d:] e: f :d][43]
复制 运算符
(将变量设置为匹配的序列)
a: [复制 b c] f: [(b: 如果 小于? 索引? e 索引? d [复制/部分 e d])][44][42]
a: [并且 [c d:] e: f :d][43]
引用 运算符,块解析(匹配终结符) a: [引用 b] a: [复制 c 跳过 (d: 除非 等于? c [b] [[失败]]) d][45][46]

表格说明了

  1. 在解析字符串时,字符串作为字符序列工作
  2. 在解析字符串时,位集作为字符选择工作
  3. 包含所有字符的位集可以定义为不包含任何字符的位集的补集
  4. 在解析字符串时,跳过 非终结符作为包含所有字符的位集工作
  5. 在解析块时,跳过 非终结符作为ANY-TYPE! 数据类型工作
  6. 可选 运算符可以使用通用选择来定义
  7. 可选 是贪婪的,因为第一个选择子表达式具有优先级
  8. a b c d 一个迭代运算符替换常见的递归非终结符:
    • 增强表达能力(节省非终结符定义)
    • 优化内存使用(节省堆栈空间)
    • 优化速度
  9. 任何 运算符可以使用通用递归表达式来定义
  10. 任何 是贪婪的,因为第一个选择子表达式具有优先级
  11. 某些 运算符可以使用通用递归表达式来定义
  12. 某些 是贪婪的,因为第一个选择子表达式具有优先级
  13. 失败 非终结符可以定义(即使没有结束 关键字!)使用某些的贪婪性
  14. 失败 版本使用结束跳过关键字更简洁,尽管
  15. 时间范围 运算符可以使用重复序列来定义
  16. 时间范围 运算符是贪婪的,因为第一个选择子表达式具有优先级
  17. 然后 运算符,增强了明显的表达能力,用于广义 TDPL
  18. 然后 运算符可以使用选择和计算的非终结符来定义
  19. "谓词" 意味着它不会推进输入位置
  20. 谓词可以使用然后运算符和失败非终结符来定义
  21. 并且 谓词可以使用位置操作来定义(警告:这不是递归安全的;非终结符 B 不得更改变量 'c 的值!)
  22. 并且 谓词可以使用谓词来定义
  23. 并且 谓词可以使用选择、序列和计算的非终结符来定义
  24. 解释:主选择的第一个子表达式是一个序列,它被设计为始终失败计算一个非前进的非终结符 C,以便 C 成功,如果 B 成功
  25. 结束 非终结符可以使用谓词来定义(注意,这不是循环定义!)
  26. 这个习语很好地解释了为什么 [结束 跳过] 习语总是失败
  27. 一些用户建议检测输入序列何时处于其头部可能很有用
  28. 开始 非终结符可以使用序列和计算的非终结符来定义
  29. 解释:除非输入位置位于输入序列的开头,否则 C 会计算为失败。
  30. to 运算符可以使用and 运算符和一个常见的递归表达式来定义。
  31. 递归定义比当前的to 运算符更通用,支持任何非终结符 B。
  32. 递归定义与to 运算符的行为相同,除了:
    • [to ""] 表达式在解析字符串时始终失败,而递归表达式始终成功
  33. a b 这是一个使用anynotfailskip 和计算出的非终结符的运算符的等效迭代定义。
  34. a b 这是一个等效的迭代定义,扩展了not 习惯用法。
  35. a b 这展示了tothru 运算符之间的关系。
  36. | END 选择会导致to 运算符始终成功。
  37. thru 运算符可以使用一个常见的递归表达式来定义。
  38. 递归定义比当前的thru 运算符更通用,支持任何非终结符 B。
  39. 递归定义与thru 运算符的行为相同,除了:
    • [thru end] 表达式始终失败,而递归表达式始终成功
    • [thru ""] 表达式在解析字符串时始终失败,而递归表达式始终成功
  40. set 运算符可以使用and 运算符、位置操作、动作和序列来定义。
  41. 请注意,此set 定义不限于块解析。
  42. a b 解释:使用 D 和 E 根据需要设置 'b 变量。请注意,如果输入位置没有向前移动,则 'b 必须设置为 NONE。
  43. a b 解释:序列中的第一个子表达式被定义,以便我们知道 B 匹配后的位置(用于设置 'd)和之前的位置(用于设置 'e)。
  44. copy 运算符可以使用and 运算符、位置操作、动作和序列来定义。
  45. quote 习惯用法使用示例:a: [copy c skip (d: unless equal? c ['hi] [[fail]]) d] 匹配字面量 'hi。
  46. 解释:除非输入的第一个元素等于给定的终结符,否则 C 会计算为失败。

如果你想使用上面的一些习惯用法,你不需要记住它们。相反,你可以使用parseen 脚本,在那里你可以找到为你生成对应规则的函数。

解析规则中的局部变量

[edit | edit source]

有时在解析规则中使用局部变量是可取的。这样的变量至少需要是递归安全的,因为解析规则经常被递归使用,但将来可能还需要线程安全的局部变量。PARSE 函数没有内置支持这种结构,但是,由于 Rebol 的可塑性,可以定义一个USE-RULE 函数,它方便在 PARSE 规则中使用(递归和线程安全)局部变量,其工作原理如下。

rule: use-rule [a b] [
    "b" (print 1) |
    a: "a" rule "a" b: (print subtract index? b index? a)
]
parse "aba" rule
parse "aabaa" rule

在 PARSE 规则中使用局部变量的更复杂示例是evaluate.r 脚本,它展示了如何在 PARSE 中处理不同的优先级和结合性规则集。

修改输入序列

[edit | edit source]

在表达式匹配期间可以操作 PARSE 输入序列,因为可以在解析操作期间使用诸如 CHANGE、INSERT 或 REMOVE 之类的序列操作函数。

以下是一些不建议在表达式匹配期间操作输入序列的原因。

  • 与其更改输入序列,不如使用一个新的序列并按照需要收集其内容。
  • 某些操作会改变当前解析的序列的长度。更改长度的操作效率低下 - 更改长度的操作需要 O(N) 时间,其中 N 是被更改序列的长度。将此与用于收集方法的 APPEND 操作进行对比,APPEND 操作的速度大约快 N 倍。
  • 更改长度的操作会破坏输入位置的簿记。这样很容易产生难以理解和调试的代码。

示例

让我们定义一个测试字符串

n: 100
random/seed 0
test-string: copy ""
repeat i n [insert tail test-string pick "abc" random 3]

让我们使用 PARSE 实现一个 remove-chars 函数。第一次尝试“就地”更改序列,但更改不会影响输入序列的长度。

remove-chars1: func [
    {Removes the given chars from a string.}
    string [string!]
    chars [char! string!] "All characters specified will be removed."
    /local chars-to-keep group change-position
] [
    ; if a char, use a string instead
    if char? chars [chars: head insert copy "" chars]
    ; define the characters we want to keep:
    chars: charset chars
    chars-to-keep: complement chars
    ; the position where the change needs to occur
    change-position: string
    ; turn off the default whitespace handling
    parse/all string [
        any [
            ; ignore chars
            any chars
            ; get a group of chars-to-keep
            copy group some chars-to-keep
            (change-position: change change-position group)
        ]
    ]
    clear change-position
    string
]

第二次尝试使用 REMOVE 函数更改输入序列的长度。

remove-chars2: func [
    {Removes the given chars from a string.}
    string [string!]
    chars [char! string!] "All characters specified will be removed."
    /local chars-to-keep group-start group-end
] [
    ; if a char, use a string instead
    if char? chars [chars: head insert copy "" chars]
    ; define the characters we want to keep:
    chars: charset chars
    chars-to-keep: complement chars
    ; turn off the default whitespace handling
    parse/all string [
        any [
            ; ignore chars-to-keep
            any chars-to-keep
            ; remove group of chars
            group-start: some chars group-end:
            (remove/part group-start group-end)
        ]
    ]
    string
]

结果是

r1: remove-chars1 copy test-string "a"
; == {cbccbbcccbbbbbcccccccbcbbcbcbcbbccbcbbccccbcbbcbbcbbcbccccbcbb}
r2: remove-chars2 copy test-string "a"
; == {cbccbbcccbababbbcccaccccbcbbaacbcbcbbccbcbbccccbcbbcbbcbbcbccccbacbb}

令人惊讶的是!使用输入操作的方法没有按预期删除所有 #"a"!问题是由不正确的输入位置处理造成的。因此,让我们固执一些,正确地处理输入位置

remove-chars3: func [
    {Removes the given chars from a string.}
    string [string!]
    chars [char! string!] "All characters specified will be removed."
    /local chars-to-keep group-start group-end
] [
    ; if a char, use a string instead
    if char? chars [chars: head insert copy "" chars]
    ; define the characters we want to keep:
    chars: charset chars
    chars-to-keep: complement chars
    ; turn off the default whitespace handling
    parse/all string [
        any [
            ; ignore chars-to-keep
            any chars-to-keep
            ; remove chars
            group-start: some chars group-end:
            (remove/part group-start group-end)
            ; set the input position properly
            :group-start
        ]
    ]
    string
]

结果是

r3: remove-chars3 copy test-string "a"
; == {cbccbbcccbbbbbcccccccbcbbcbcbcbbccbcbbccccbcbbcbbcbbcbccccbcbb}

速度讨论

[edit | edit source]

由于 CHANGE 函数(或任何其他 Rebol 原生函数)不允许在不复制的情况下移动序列的一部分,因此 REMOVE-CHARS1 函数必须做很多额外的工作,分配和收集很多组字符串,而 REMOVE-CHARS3 函数使用 REMOVE 原生的全部速度,不需要分配或释放任何额外的“辅助”字符串。

证明 REMOVE-CHARS1 函数算法质量的一个证明是,即使在这样不利的条件下,它的速度仍然具有竞争力,即使对于最短的字符串也是如此,并且对于短至 600 个字符的字符串,REMOVE-CHARS1 函数变得比 REMOVE-CHARS3 函数更快。对于更长的字符串,速度差异将更加有利于 REMOVE-CHARS1 函数。

故障排除

[edit | edit source]

PARSE 是一个非常强大的函数,但如果你没有密切关注你在做什么,它也可能很麻烦。如果你不幸的话,PARSE 会陷入无限循环,需要你重新启动 Rebol。

但它究竟是什么时候发生的?

PARSE 通常会遍历块,但真正使 PARSE 进步的是表达式。如果你指定一个不会使其进步的表达式,它将永远停留在同一个点。

这样的表达式可以是重复匹配空序列、空选择子表达式或 NONE 表达式的表达式。

示例

>> parse "abc" [any []]
*** HANGS Rebol ***
>> parse "abc" [some ["a" |]]
*** HANGS Rebol ***
>> parse "abc" [some [none]]
*** HANGS Rebol ***

注意:为了能够从无限循环中逃脱,请在迭代表达式中使用 (),例如

 >> parse "abc" [any [()]]
 *** YOU CAN PRESS [ESC] NOW TO STOP THE LOOP ***
华夏公益教科书