跳至内容

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

来自维基教科书,开放的书籍,开放的世界

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

即使在分割字符串时,您也可能需要这样做,但不希望将引号专门作为 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 惯用法。

Paren

[edit | edit source]

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

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

注意

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

无参数的 PARSE 关键字

[edit | edit source]

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

end 关键字

[edit | edit source]
parse "" [end]
; == true

这将返回 TRUE,因为

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

skip 关键字

[edit | edit source]
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

[edit | edit source]

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

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

不同的单词匹配将失败

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

注意

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

路径的工作方式类似于单词,即查找并使用路径的值进行匹配。

Lit-path

[edit | edit source]

Lit-path 的工作方式类似于 lit-word,即它们匹配输入代码块中的相应路径。

注意

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

字符集

[edit | edit source]

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

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 值也可以将当前输入位置向前移动。
  • 在代码块解析的情况下,位集的行为类似于终结符

Datatype

[edit | edit source]

在解析代码块时,我们可以使用 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

注意

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

Set-word

[edit | edit source]

Set-word 被视为非终结符,并用于获取当前输入位置。Set-word 始终成功,不移动输入位置

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

说明

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

注意

  • 由于 set-word 被视为非终结符,因此它们不能用作终结符 来匹配输入代码块中的特定 set-word。要匹配特定的 set-word,请参见解析习惯用法 部分中的 quote 惯用法。

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

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

说明

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

注意

  • 由于 get-words 被视为非终结符,它们不能用作终结符来匹配输入块中的特定 get-words。要匹配特定的 get-words,请参见解析习语部分中的引号习语。

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

其他数据类型

[编辑 | 编辑源代码]

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

解析操作

[编辑 | 编辑源代码]

让我们看看 PARSE 如何遍历块

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

这里哪里出错了?发生的情况是,PARSE 成功地匹配了 INPUT 块中的第一个词,并将输入前进到第二个词。

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

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

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

subexpression_1 subexpression_2 ... subexpression_n

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

示例

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

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

有序选择

[编辑 | 编辑源代码]

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

subexpression_1 | subexpression_2 | ... | subexpression_n

在匹配有序选择时,PARSE 尝试匹配 subexpression_1。如果成功,则有序选择匹配成功。如果第一个 subexpression 匹配不成功,则 PARSE 尝试匹配选择中的其余部分。

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

e1 e2 | e3

等效于

[e1 e2] | e3

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

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

注意:有序选择操作的优先级导致

["a" | "ab"]

中的第二个 subexpression 永远不会成功,因为第一个具有优先权。

重复操作符

[编辑 | 编辑源代码]

重复操作符指定给定 subexpression 应该匹配多少次。重复的一般语法是

 repetition_operator subexpression

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

repetition_operator subexpression_1 subexpression_2

[repetition_operator subexpression_1] subexpression_2

相同。所有重复操作符都是贪婪的,这意味着它们始终尽可能多地匹配。

重复操作符是左结合的,这意味着

any 2 skip

等效于

[any 2] skip

tothruinto 操作符也是如此。

零次或一次

[编辑 | 编辑源代码]

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

示例

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

一次或多次

[编辑 | 编辑源代码]

此操作符使用some关键字。

示例

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

零次或多次

[编辑 | 编辑源代码]

此操作符使用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操作符成功地只匹配了输入的第一个元素,然后停止,没有到达尾部

重复计数

[编辑 | 编辑源代码]

此操作的一般形式是

n subexpression

其中 N 是一个整数值。此操作指定给定 subexpression 的重复计数。

示例

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

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

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

次数范围

[编辑 | 编辑源代码]

此操作的一般形式是

n m subexpression

其中 N 和 M 是整数值。此操作指定 subexpression 的重复范围。

示例

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 操作符根据给定的解析 subexpression 推进输入位置

  • to操作符
  • thru操作符

操作的一般语法是

to subexpression

thru subexpression

to操作符的目的是将输入位置推进直到成功匹配 subexpression 的位置。

thru操作符的目的是将输入位置推进到成功匹配 subexpression 之后的位置

如果未找到成功的 subexpression 匹配,则这两个操作都会失败。

subexpression 可以是

  • 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

子块解析

[edit | edit source]

解析块时,你可能需要检查它的子块(或括号、路径、字面量路径或获取路径)是否存在特定模式。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

使用输入序列中的数据

[edit | edit source]

你也可以使用序列中的数据来用于你的触发代码。除了使用设置词来获取输入位置之外,还有以下 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

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

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 解析之间的区别

[edit | edit source]

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 子规则 - R3 parse 的新关键字,由 Gabriele 提议

复杂的解析表达式

[edit | edit source]

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

递归

[edit | edit source]

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

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

用法

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

解析习语

[edit | edit source]
描述 操作 习语
Any-string,字符串解析 a: ["abc"] a: [#"a" #"b" #"c"][1]
Bitset,字符串解析 a: charset ",;" a: [#"," | #";"][2]
skip 非终结符,字符串解析 a: [skip] b: complement charset ""[3]
a: [b][4]
skip 非终结符,块解析 a: [skip] a: [any-type!][5]
opt 运算符(零或一) a: [opt b] a: [b |][6][7]
any 运算符(零个或多个)[8] a: [any b] a: [b a |][9][10]
some 运算符(一个或多个)[8] a: [some b] a: [b [a |]][11][12]
fail(始终失败的非终结符) a: [fail] a: [some "a" "a"][13]
a: [end skip][14]
次数范围 运算符 a: [m n b] a: [(l: min m n k: n - m) l b [k [b | c: fail] | :c]][15][16]
then 运算符[17]
(匹配 B,如果成功,则匹配 C;否则匹配 D)
a: [b then c | d] a: [[b (e: c) | (e: d)] e][18]
not 谓词[19](反转成功) a: [not b] a: [b then fail |][20]
a: [[b (c: [fail]) | (c: none)] c]
and 谓词(匹配但不前进) a: [and b] a: [c: b :c][21]
a: [not not b][22]
a: [[b (c: none) | (c: [fail])] fail | c][23][24]
end 非终结符(匹配输入的尾部) a: [end] a: [not skip][25][26]
start 非终结符(匹配输入的头部)[27] a: [start] a: [b: (c: unless head? b [[fail]]) c][28][29]
to 运算符[8]
(前进到第一个成功的匹配)
a: [to b] a: [and b | skip a][30][31][32]
a: [any [not [b (c: none)] (c: [fail]) skip] c][33]
a: [any [[b (c: none d: [fail]) | (c: [fail] d: [skip])] d] c][34]
a: [thru [and b]][35]
toany 之间的对应关系 a: [to [b | end]][36] a: [any [not b skip]]
thru 运算符[8]
(前进到第一个成功的匹配)
a: [thru b] a: [b | skip a][37][38][39]
a: [any [not [b c: (d: [:c])] (d: [fail]) skip] d][33]
a: [any [[b c: (d: [:c] e: [fail]) | (d: [fail] e: [skip])] e] d][34]
a: [to [b c:] :c][35]
set 运算符
(将变量设置为第一个匹配的值)
a: [set b c] f: [(set/any [b] if lesser? index? e index? d [e])][40][41][42]
a: [and [c d:] e: f :d][43]
copy 运算符
(将变量设置为匹配的序列)
a: [copy b c] f: [(b: if lesser? index? e index? d [copy/part e d])][44][42]
a: [and [c d:] e: f :d][43]
quote 运算符,块解析(匹配终结符) a: [quote b] a: [copy c skip (d: unless equal? c [b] [[fail]]) d][45][46]

该表格说明了

  1. 解析字符串时,字符串充当字符序列
  2. 解析字符串时,位集充当字符选择
  3. 包含所有字符的位集可以定义为不包含任何字符的位集的补集
  4. 解析字符串时,skip 非终结符充当包含所有字符的位集
  5. 解析块时,skip 非终结符充当ANY-TYPE! 数据类型
  6. opt 运算符可以使用常见的选择来定义
  7. opt 是贪婪的,因为第一个选择子表达式优先
  8. a b c d 一个迭代运算符替换一个常见的递归非终结符:
    • 增强表达能力(节省非终结符定义)
    • 优化内存使用(节省堆栈空间)
    • 优化速度
  9. any 运算符可以使用常见的递归表达式来定义
  10. any 是贪婪的,因为第一个选择子表达式优先
  11. some 运算符可以使用常见的递归表达式来定义
  12. some 是贪婪的,因为第一个选择子表达式优先
  13. fail 非终结符可以定义(即使没有end 关键字!) 使用some 的贪婪性
  14. fail 版本使用endskip 关键字更简洁,虽然
  15. 时间范围 运算符可以使用重复序列来定义
  16. 时间范围 运算符是贪婪的,因为第一个选择子表达式优先
  17. then 运算符,增强了明显的表达能力,用在广义 TDPL
  18. then 运算符可以使用选择和计算的非终结符来定义
  19. "谓词" 意味着它不会推进输入位置
  20. not 谓词可以使用then 运算符和 fail 非终结符来定义
  21. and 谓词可以使用位置操作来定义(警告:这不是递归安全的;非终结符 B 必须不改变变量 'c 的值!)
  22. and 谓词可以使用not 谓词来定义
  23. and 谓词可以使用选择、序列和计算的非终结符来定义
  24. 解释:主选择的第一个子表达式是一个序列,它被设计为始终失败,计算一个非推进的非终结符 C,以便 C 成功,如果 B 成功
  25. end 非终结符可以使用not 谓词来定义(注意这不是一个循环定义!)
  26. 这种习惯用法很好地解释了为什么 [end skip] 习惯用法总是失败
  27. 一些用户建议,检测输入序列是否处于头部可能很有用
  28. start 非终结符可以使用一个序列和一个计算的非终结符来定义
  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 ***
华夏公益教科书