开始之前

为什么是 grammars?

Grammars 解析字符串并从这些字符串返回数据结构。Grammars 可用于编写执行程序以确定程序是否可以运行(如果它是一个有效的程序),将网页分解成组成部分,或在其它的东西中识别句子的不同部分。

我什么时候该使用 grammars?

如果你有驯服或解释的字符串,grammar 提供工具来完成这项工作。

该字符串可能是一个文件, 您想把它拆分成多个章节; 也许是一个协议,比如 SMTP,你需要指定哪些“命令”来自用户提供的数据;也许你正在设计自己的领域特定语言。Grammars 可以提供帮助。

grammars 的广义概念

正则表达式(Regexes)适用于查找字符串中的模式。然而,对于一些任务来说,如同时查找多个模式,或者组合模式,或者单独测试可能围绕字符串正则表达式的模式是不够的。

在使用 HTML 时,您可以定义一个 grammar 来识别 HTML 标记,包括开始和结束元素以及它们之间的文本。然后,您可以将这些元素组织到数据结构中,例如数组或散列。

Grammar 指南

你总是会遇到令人头疼的字符串解析。举个例子, 据说 HTML 不能被有效地分解和解析,只需使用正则表达式来排序元素。另一个例子是定义单词和符号可能构成语言并提供含义的顺序。这正 和 Perl 的 Gramamr 系统完美契合。

Grammar 非常适合接受字符串,试图理解它们,然后将它们保存到一个你实际可以使用的数据结构中。如果你有某种带顺序或解释类型的字符串,Grammar 给你一些很强大的工具,使解析字符串更容易。

你的字符串可能是整个文件,你需要分成几个部分。也或许是一行一行的。也许你有一个正在使用的 SMTP 那样的协议,想要一个方便有条理的方式来定义哪些“命令”需要在用户数据的后面,使协议工作。也许你想创建自己的基于字符串的协议。也许你正在设计自己的语言。

正则表达式(regex)很好地在字符串中查找模式并操作它们。然而,当你需要同时找到多个模式,或者需要组合模式,或者测试可能围绕字符串的模式或其他模式 - 单单用正则表达式是不够的。

Grammar 提供了一种方式来定义如何使用正则表达式来检查字符串,并且可以将这些正则表达式组合在一起以提供更多的意义。

例如,在HTML的情况下,您可以定义一个语法,它可以识别HTML标记(开始和结束元素以及它们之间的文本),并通过将这些元素填充到数据结构中来对这些元素进行操作,例如数组或散列,然后可以轻松使用。实质上,Grammar 提供了一种定义可用于解析任意大小和复杂度的字符串的完整语言或规范的手段。

更多 Grammar 技术

概念描述

Gramamr 被定义为对象, 就像 Perl 中的其它东西。从技术上讲, Gramamr 是普通的类加上一点额外的魔法, 我们稍后就说到它 — 还有一点限制。你像类那样命名和定义一个 Grammar, 除了使用「grammar」关键字代替「class」。

  1. grammar My::Gram { ..methods 'n stuff... }

Grammar 包含像方法那样的元素, 这些方法叫做 regex, tokenrule。这些方法是有名字的, 就像方法有名字一样。它们每一个都定义一个 regex, token 或 rule(它们几乎是同样的东西(并不真的一样))。

一旦你定义了你的 Grammar, 在你的程序中通过 Grammar 的名字调用它并传递你想解析的字符串。该字符串将通过你的 regex, token 和 rule “方法”定义的规则运行。 完成后,将返回一个 Match 对象,该对象已填充了用于定义方法的名称所结构化并存储的数据。

  1. my $matchObject = My::Gram.parse($what-a-big-string-you-have);

现在,你可能想知道,如果我让所有这些定义的正则表达式只返回他们的结果,那么这该如何帮助在字符串中向前或向后解析东西呢,或需要从多个那样的正则表达式组合的东西。 ..这就是 grammar action 发挥作用的地方。

对于你的 grammar 中匹配的每个“方法”,你会得到一个可调用的动作,用那个匹配你可以做一些有趣或聪明的事情。 你还可以得到一个最重要的 action,你可以使用这个 action 把它们捆绑在一起,并自定义构建一个你可能想要返回的数据结构,其中所有疯狂的字符串解析在你很好的排序和定义的数据结构是有意义的。 默认情况下,此 over-arching 方法称为 TOP。 我们也会得到更多的。

技术概览

Grammars 就像类那样定义, 除了使用 grammar 关键字代替 class. grammars 中的「methods」叫做 regex, token, 或 rule。虽然 Regex 方法慢但是彻底 — 它们会在字符串中向后查看并真的尝试匹配。Token 方法更快一点并且它们忽略空白。Rule 方法和 token 方法一样, 但是它们在你的”regex” 定义中消费空白。

当方法(regex, token 或 rule)在 grammar 中匹配后, 匹配到的字符串被放入最终将返回的 Match 对象中, 并且它将使用与您选择命名的方法相同的名称。

  1. grammar My::Gram {
  2. token TOP { <thingy> .* }
  3. token thingy { 'clever_text_keyword' }
  4. }

所以在这里,如果你写 my $match = My::Gram.parse($string) - 并且你的字符串以 ‘clever_text_keyword’ 开头, 那么你会得到一个匹配对象,在你的匹配对象中包含用「thingy」 标记的 ‘clever_text_keyword’ 字符串。 这些可以变得越来越复杂,根据你的需要,如你所想。

现在, 我们说说 TOP。 TOP 方法(regex, token or rule)是必须匹配一切的(默认)的包罗万象的 regex。 如果传递进来解析的字符串与 TOP regex 不匹配,则返回的匹配对象将为空(Any)。

正如你可以看到的,在 TOP 中,提到了 <thingy> 标记。 <thingy> 被定义在下一行,token thingy …​。 这意味着 ‘clever_text_keyword’ 必须是传入的字符串中的第一个东西,否则 grammar 解析将失败,而我们将得到一个空匹配。 这对于识别有人可能给你应该被丢弃的畸形的东西是极好的。

通过一个例子学习 Grammar - REST 设计

让我们假设我们要将一个 URL 解析成组成 RESTful 请求的组件部分。假设我们希望网址的工作方式如下:

  • URI 的第一部分,我们称之为“主体”,如零件,产品或人。

  • URI 的第二部分,我们称之为“命令”,就像标准的 CRUD 东西(创建,检索,更新或删除)。

  • URI 的第三部分将是任意数据。也许我们将使用的具体ID,或者一个由“/”分隔的长列表数据。

  • 当我们得到一个URL时,我们需要把上面的1-3放在一个很好的我们可以使用的数据结构中,而不必做各种分割,并且可以很容易地在未来改变或扩展(或扩展) 。

因此,如果我们在服务器上有一个 “/product/update/7/notify” 的 URI,我们希望我们的 Grammar 给我们一个很好的 $match 对象,它有一个“product”“subject”,一个”更新““command和”7/notify“的”数据“(现在)。

我们做的第一件事是定义 grammar 类。我们将需要定义我们的主题,命令和数据。我想我们将为他们使用 token,因为我们不关心正则表达式中的空格。

  1. grammar REST {
  2. token subject { \w+ }
  3. token command { \w+ }
  4. token data { .* }
  5. }

到目前为止,这个 REST Grammar 说,我们想要一个只是单词字符的主题,一个只是单词字符的命令和剩余全部是字符串的数据(在这种情况下为 URI)。

但是在我们的大字符串中,我们不知道这些正则表达式匹配将会进入什么顺序。我们需要能够将这些匹配的 token 放在我们将作为该字符串传递的URI的更大的上下文中。 这就是 TOP 方法要做的。 因此,我们添加 TOP,并在其中放置我们的 token 名称,以及其它应该出现的有效字符串。

  1. grammar REST {
  2. token TOP { '/' <subject> '/' <command> '/' <data> }
  3. token subject { \w+ }
  4. token command { \w+ }
  5. token data { .* }
  6. }

实际上,您可以用它从基本的 CRUD 的 URI 中提取您的数据,其中包含所有3个参数:

  1. my $match = REST.parse('/product/update/7/notify');
  2. say $match;

输出:

  1. «「/product/update/7/notify
  2. subject => product
  3. command => update
  4. data => 7/notify」»

当然,可以使用 $match<subject>$match<command>$match<data> 直接访问数据以返回解析的值。 它们每个都包含可以进一步工作的匹配对象,或强制转换为字符串($match<command>.Str

添加一点灵活性

到目前为止,REST语法将处理检索,删除和更新。 但是,create 命令没有第三部分(数据部分)。 这意味着如果我们尝试解析 creat URL,我们的 Grammar 将无法匹配。 为了避免这种情况,我们需要使最后一个数据位置匹配可选,以及它前面的’/‘。 这很容易通过为分组的’/‘和 TOP token 的数据组件添加一个问号来表示它们的可选性质,就像一个普通的正则表达式那样。 所以现在我们有:

  1. grammar REST {
  2. token TOP { '/' <subject> '/' <command> [ '/' <data> ]? }
  3. token subject { \w+ }
  4. token command { \w+ }
  5. token data { .* }
  6. }
  7. my $m = REST.parse('/product/create');
  8. say $m<subject>, $m<command>;
  9. # OUTPUT: «「product」「create」
  10. »

让我们想象,为了演示的目的,我们可能想允许用户从终端输入这些相同的 URI。 在这种情况下,他们可能在’/‘之间放置空格,因为用户容易破坏事物。 如果我们想要适应这种可能性,我们可以用另一个 token 替换 TOP 中的 ‘/‘,以允许在它的任何一边的空格。

  1. grammar REST {
  2. token TOP { <slash><subject><slash><command>[<slash><data>]? }
  3. token subject { \w+ }
  4. token command { \w+ }
  5. token data { .* }
  6. token slash { \s* '/' \s* }
  7. }
  8. my $m = REST.parse('/ product / update /7 /notify');
  9. say $m;
  10. # OUTPUT: «「/ product / update /7 /notify」
  11. # slash => 「/ 」
  12. # subject => 「product」
  13. # slash => 「 / 」
  14. # command => 「update」
  15. # slash => 「 /」
  16. # data => 「7 /notify」»

现在我们在我们的匹配对象中得到一些额外的垃圾,即那些斜线,但有一些非常好的方法,使我们得到一个整洁的返回值。

添加一些约束

我们希望我们的 RESTful Grammar 只允许 CRUD 操作。 还有我们想要解析的东西。 这意味着我们上面的“命令”应该有四个值之一:create, retrieve, update 或 delete.。

有几种方法来完成这个。 例如,您可以更改 command 方法:

  1. token command { \w+ }
  2. # ...becomes...
  3. token command { 'create'|'retrieve'|'update'|'delete' }

要成功解析 URI,/ 之间的字符串的第二部分必须是那些 CRUD 值之一,否则解析失败。这正是我们想要的。

还有另一种技术可以在选项膨胀时提供更大的灵活性并提高可读性:原型正则表达式(proto-regexes)。

为了利用这些原型正则表达式(实际上是 multi methods)将我们限制为有效的 CRUD 选项,我们将用以下代替 token command:

  1. proto token command {*}
  2. token command:sym<create> { <sym> }
  3. token command:sym<retrieve> { <sym> }
  4. token command:sym<update> { <sym> }
  5. token command:sym<delete> { <sym> }

sym 关键字用于创建各种原型正则表达式(proto-regex)选项。每个选项都被命名(例如, sym<update>), 并且为了使用该选项,会使用相同的名字自动生成一个特殊的 <sym> token。

可以在原型正则表达式选项块中使用 <sym> token 以及其他用户定义的 tokens 来定义特定的“匹配条件”。正则表达式 tokens 是编译过的形式,一旦定义,随后就不能被副词动作(例如: i)修改。因此,由于它是自动生成的,所以特殊的 <sym> token 仅在需要与选项名称完全匹配时才有用。

如果对于其中一个原型正则表达式选项,出现匹配条件,则整个原型的搜索终止。匹配数据以匹配对象的形式分配给父原型 token。如果使用特殊 <sym> token,并形成全部或部分实际匹配,则将其保留为匹配对象中的子级别,否则它将不存在。

使用这样的原型正则表达式给了我们很大的灵活性。例如,不是返回 <sym>,在这种情况下是匹配的整个字符串,我们可以输入自己的字符串,或做其他有趣的事情。我们可以用“token subject”方法做同样的事,并将其限制为仅对有效主题(如’part’或’people’等)进行正确解析。

把我们的 RESTful Grammar 组合在一块

目前为止我们的 RESTful URIs 的处理如下:

  1. grammar REST
  2. {
  3. token TOP { <slash><subject><slash><command>[<slash><data>]? }
  4. proto token command {*}
  5. token command:sym<create> { <sym> }
  6. token command:sym<retrieve> { <sym> }
  7. token command:sym<update> { <sym> }
  8. token command:sym<delete> { <sym> }
  9. token subject { \w+ }
  10. token data { .* }
  11. token slash { \s* '/' \s* }
  12. }

让我们看看各种 URI,以及它们在通过我们的 Grammar 时是如何表现的。

  1. my @uris = ['/product/update/7/notify',
  2. '/product/create',
  3. '/item/delete/4'];
  4. for @uris -> $uri {
  5. my $m = REST.parse($uri);
  6. say "Sub: $m<subject> Cmd: $m<command> Dat: $m<data>";
  7. }
  8. # OUTPUT: «Sub: product Cmd: update Dat: 7/notify
  9. # Sub: product Cmd: create Dat:
  10. # Sub: item Cmd: delete Dat: 4»

请注意,由于 <data> 与第二个字符串没有匹配,因此 $m<data> 将为 Nil,然后在 say 函数的字符串上下文中使用它会发出警告。

只用 grammar 的这一部分,我们就能获得几乎所有我们正在寻找的东西。 URI 被解析,我们得到一个数据结构。

data token 将 URI 的整个末尾作为一个字符串返回。 4 很好。但是从 ‘7/notify’ 中我们只需要那个 7。为了得到 7,我们将使用 grammar 类的另一个特性: actions。

Grammar Actions

在 Grammar 类中使用 Grammar actions 来处理匹配。Actions 在它们自己的类中定义,与 grammar 类不同。

您可以将 grammar action 看作 grammar 插件扩展模块的一种。很多时候你都会很开心的使用 grammars。但是当你需要进一步处理其中的一些字符串时,你可以插入 Actions 扩展模块。

要使用 action,可以使用名为 actions 的命名参数,它应该包含 action 类的一个实例。通过上面的代码,如果我们的 action 类调用了 REST-actions,我们会像这样解析 URI 字符串:

  1. my $matchObject = REST.parse($uri, actions => REST-actions.new);
  2. # …or if you prefer…
  3. my $matchObject = REST.parse($uri, :actions(REST-actions.new));

如果你将你的 action 方法命名为与你的 grammar 方法(tokens,regexes,rules)相同的名称,那么当您的 grammar 方法匹配时,具有相同名称的 action 方法将自动调用。该方法还将传递相应的匹配对象(由 $/ 变量表示)。

我们来看一个例子。

我们回到我们离开的地方:

  1. grammar REST
  2. {
  3. token TOP { <slash><subject><slash><command>[<slash><data>]? }
  4. proto token command {*}
  5. token command:sym<create> { <sym> }
  6. token command:sym<retrieve> { <sym> }
  7. token command:sym<update> { <sym> }
  8. token command:sym<delete> { <sym> }
  9. token subject { \w+ }
  10. token data { .* }
  11. token slash { \s* '/' \s* }
  12. }

回想一下,我们想要进一步处理 data token “7/notify”, 以获得 7. 为此,我们将创建一个与具名 token 名称相同的方法的 action 类。在这种情况下,我们的 token 被命名为 data,因此我们的方法也被命名为 data

  1. class REST-actions
  2. {
  3. method data($/) { $/.split('/') }
  4. }

现在,当我们通过 Grammar 传递 URI 字符串时,data token 匹配将传递给 REST-actions 的 data 方法。action 方法会按照 / 字符拆分字符串,返回列表的第一个元素将是 ID 号 (即 “7/notify” 中的 7)。

但你高兴的太早了。

用 “make” 和 “made” 使 grammars 保持整洁

如果 grammar 在 data 上调用上面的 action,那么 data 方法将被调用,但是返回到程序的大的 TOP grammar 匹配结果中不会显示任何内容。 为了使 action 的结果显示出来,我们需要在这个结果上调用 make,这个结果可以是很多东西,包括字符串,数组或散列结构。

你可以想象,make 把该结果存到 grammar 中一个特殊的容器化区域中。 我们所制作(make)的所有东西,稍后都可以通过 made 来访问。

因此,代替我们的上面的 REST-actions 类,我们应该写:

  1. class REST-actions
  2. {
  3. method data($/) { make $/.split('/') }
  4. }

当我们为 match split(它返回一个列表)中添加 make 时,这个 action 将返回一个数据结构给我们的 grammar,它将与原 grammar 的 data token 分开存储。 这样,如果我们需要,我们可以操作两者。

如果我们想从这个长的 URI 中访问 7 这个 ID, 那么我们访问从我们所制成的(made)的 data action 返回的列表的第一个元素:

  1. my $uri = '/product/update/7/notify';
  2. my $match = REST.parse($uri, actions => REST-actions.new);
  3. say $match<data>.made[0]; # OUTPUT: «7
  4. »
  5. say $match<command>.Str; # OUTPUT: «update
  6. »

在这里,我们在 data 上调用 made,因为我们想要我们所制成的(made)(使用 make)action 的结果以得到分割后的数组。这好极了!但是,如果我们能够构造(make)一个包含我们想要的所有东西的更友好的数据结构,而不是强转类型和牢记数组,是不是更好?

就像 Grammar 中匹配整个字符串的 TOP, actions 也有一个 TOP 方法。我们可以构造(make)所有单独的匹配组件,如 datasubjectcommand,然后我们可以将它们放置在我们将在 TOP 中构造(make)的数据结构中。当我们返回最终的匹配对象时,之后就可以访问该数据结构了。

要做到这一点,我们要做的是将方法 TOP 添加到 action 类中,在该方法中,从组件片段中构造(make)出我们喜欢的任何数据结构。

所以,我们的 action 类现在变成:

  1. class REST-actions
  2. {
  3. method TOP ($/) {
  4. make { subject => $<subject>.Str,
  5. command => $<command>.Str,
  6. data => $<data>.made }
  7. }
  8. method data($/) { make $/.split('/') }
  9. }

在我们的 TOP 方法中,subject 与我们在 grammar 中匹配的 subject 保持相同。 此外, command 返回匹配到的(create, update, retrieve, 或 delete)的有效 <sym>。 我们把每个匹配都强转为 .Str,因为我们不需要整个匹配对象。

但是我们想要确定的是,在 $<data> 对象上使用 made 方法,因为我们想要访问那个我们在 action 中使用 make 制成的(made)的分割,而不是正确的 $<data> 对象。

我们在 grammar action 的 TOP 方法中构造(make)一些东西之后,我们可以通过在 grammar 结果对象上通过调用 made 方法来访问所有的自定义值。 代码现在变成:

  1. my $uri = '/product/update/7/notify';
  2. my $match = REST.parse($uri, actions => REST-actions.new);
  3. my $rest = $match.made;
  4. say $rest<data>[0]; # OUTPUT: «7
  5. »
  6. say $rest<command>; # OUTPUT: «update
  7. »
  8. say $rest<subject>; # OUTPUT: «product
  9. »

如果你不需要完整的返回匹配对象,你可以从你的 actions 的 TOP 方法中只返回 made 后的数据。

  1. my $uri = '/product/update/7/notify';
  2. my $rest = REST.parse($uri, actions => REST-actions.new).made;
  3. say $rest<data>[0]; # OUTPUT: «7
  4. »
  5. say $rest<command>; # OUTPUT: «update
  6. »
  7. say $rest<subject>; # OUTPUT: «product
  8. »

哦,我们忘了摆脱那个丑陋的数组元素编号了吗? 嗯。 让我们在 TOP grammar 的自定义返回中构造(make) 一个新东西 - 我们称之为 subject-id,并将它设置为 <data> 的第0个元素。

  1. class REST-actions
  2. {
  3. method TOP ($/) {
  4. make { subject => $<subject>.Str,
  5. command => $<command>.Str,
  6. data => $<data>.made,
  7. subject-id => $<data>.made[0] }
  8. }
  9. method data($/) { make $/.split('/') }
  10. }

现在我们可以这样做:

  1. my $uri = '/product/update/7/notify';
  2. my $rest = REST.parse($uri, actions => REST-actions.new).made;
  3. say $rest<command>; # OUTPUT: «update
  4. »
  5. say $rest<subject>; # OUTPUT: «product
  6. »
  7. say $rest<subject-id>; # OUTPUT: «7
  8. »

下面是完整的代码:

  1. grammar REST
  2. {
  3. token TOP { <slash><subject><slash><command>[<slash><data>]? }
  4. proto token command {*}
  5. token command:sym<create> { <sym> }
  6. token command:sym<retrieve> { <sym> }
  7. token command:sym<update> { <sym> }
  8. token command:sym<delete> { <sym> }
  9. token subject { \w+ }
  10. token data { .* }
  11. token slash { \s* '/' \s* }
  12. }
  13. class REST-actions
  14. {
  15. method TOP ($/) {
  16. make { subject => $<subject>.Str,
  17. command => $<command>.Str,
  18. data => $<data>.made,
  19. subject-id => $<data>.made[0] }
  20. }
  21. method data($/) { make $/.split('/') }
  22. }

直接添加 actions

上面我们看到如何将 grammars 与 actions 对象相关联,并在匹配对象上执行 actions。但是,当我们想要处理匹配对象时,这不是唯一的方法。看下面的例子:

  1. grammar G {
  2. rule TOP { <function-define> }
  3. rule function-define {
  4. 'sub' <identifier>
  5. {
  6. say "func " ~ $<identifier>.made;
  7. make $<identifier>.made;
  8. }
  9. '(' <parameter> ')' '{' '}'
  10. { say "end " ~ $/.made; }
  11. }
  12. token identifier { \w+ { make ~$/; } }
  13. token parameter { \w+ { say "param " ~ $/; } }
  14. }
  15. G.parse('sub f ( a ) { }');
  16. # OUTPUT: «func f
  17. param a
  18. end f
  19. »

这个例子是解析器的缩版。让我们更专注于它显示的功能。

首先,我们可以在 grammar 本身中添加 action,一旦正则表达式的控制流到达它们,就会执行这些 action。请注意,action 对象的方法将始终在整个正则表达式项匹配后执行。其次,它展示了 make 真正做了什么,它不过是 $/.made = …​ 的语法糖。这个技巧引入了一种从正则表达式 item 中传递消息的方法。

希望这有助于向您介绍 Raku 中的 Grammar,并向您展示 grammar 和 grammar action 类是如何协同工作的。有关更多信息,请查看更高级的 Perl Grammar指南

对于更多的 Grammar 调试,请参见 Grammar::Debugger。它为每个 grammar tokens 提供了断点调试和颜色高亮的匹配(MATCH)和匹配失败(FAIL)的输出。