开始之前§

为什么使用语法?§

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

我什么时候会使用语法?§

如果您需要驯服或解释字符串,语法提供了完成此工作的工具。

该字符串可以是您要分解成部分的文件;也许是一个协议,比如 SMTP,您需要指定哪些“命令”在哪些用户提供的数据之后;也许您正在设计自己的领域特定语言。语法可以提供帮助。

语法的广义概念§

正则表达式 (Regexes) 非常适合在字符串中查找模式。但是,对于某些任务,例如一次查找多个模式,或组合模式,或测试可能围绕字符串的模式,仅使用正则表达式是不够的。

在处理 HTML 时,您可以定义一个语法来识别 HTML 标签,包括开始和结束元素,以及它们之间的文本。然后,您可以将这些元素组织成数据结构,例如数组或哈希表。

深入技术§

概念概述§

语法是一种特殊的类。您可以像声明和定义任何其他类一样声明和定义语法,只是您使用grammar关键字而不是class关键字。

grammar G { ... }

作为这样的类,语法由定义正则表达式、标记或规则的方法组成。这些都是不同类型匹配方法的变体。定义语法后,您可以调用它并将字符串传递给它进行解析。

my $matchObject = G.parse($string);

现在,您可能想知道,如果我定义了所有这些正则表达式,它们只返回结果,那么这如何帮助解析可能在另一个字符串中向前或向后,或者需要从这些正则表达式中组合起来的东西呢?... 这就是语法操作发挥作用的地方。

对于您在语法中匹配的每个“方法”,您都会获得一个操作,您可以使用它来对该匹配进行操作。您还会获得一个总体操作,您可以使用它来将所有匹配项绑定在一起并构建数据结构。默认情况下,此总体方法称为TOP

技术概述§

如前所述,语法使用grammar关键字声明,其“方法”使用regextokenrule声明。

  • 正则表达式方法速度慢但彻底,它们会回溯字符串并真正尝试。

  • 标记方法比正则表达式方法快,并且忽略空格。标记方法不会回溯;它们在第一次可能的匹配后放弃。

  • 规则方法与标记方法相同,只是不忽略空格。

当方法(正则表达式、标记或规则)在语法中匹配时,匹配的字符串将放入一个匹配对象中,并使用与方法相同的名称作为键。

grammar G {
    token TOP { <thingy> .* }
    token thingy { 'clever_text_keyword' }
}

如果您要使用my $match = G.parse($string),并且您的字符串以 'clever_text_keyword' 开头,您将获得一个匹配对象,其中包含 'clever_text_keyword',并使用您匹配对象中<thingy>的名称作为键。例如

grammar G {
    token TOP { <thingy> .* }
    token thingy { 'Þor' }
}
 
my $match = G.parse("Þor is mighty");
say $match.raku;     # OUTPUT: «Match.new(made => Any, pos => 13, orig => "Þor is mighty",...» 
say $/.raku;         # OUTPUT: «Match.new(made => Any, pos => 13, orig => "Þor is mighty",...» 
say $/<thingy>.raku;
# OUTPUT: «Match.new(made => Any, pos => 3, orig => "Þor is mighty", hash => Map.new(()), list => (), from => 0)␤» 

前两行输出显示$match包含一个Match对象,其中包含解析结果;但这些结果也被分配给匹配变量$/。任何匹配对象都可以使用thingy作为键,如上所示,以返回该特定token的匹配项。

TOP方法(无论是正则表达式、标记还是规则)都是必须匹配所有内容(默认情况下)的总体模式。如果解析的字符串与 TOP 正则表达式不匹配,您返回的匹配对象将为空 (Nil)。

如您在上面看到的,在TOP中,提到了<thingy>标记。<thingy>在下一行定义。这意味着'clever_text_keyword'**必须**是字符串中的第一个内容,否则语法解析将失败,我们将获得一个空匹配。这对于识别应该丢弃的格式错误的字符串非常有用。

从示例学习 - REST 技巧§

假设我们想将 URI 解析成构成 RESTful 请求的各个组成部分。我们希望 URI 能够像这样工作

  • URI 的第一部分将是“主题”,例如部件、产品或人员。

  • URI 的第二部分将是“命令”,标准的 CRUD 函数(创建、检索、更新或删除)。

  • URI 的第三部分将是任意数据,可能是我们正在处理的特定 ID 或由“/”分隔的长数据列表。

  • 当我们获得 URI 时,我们希望将上述 1-3 部分放入一个数据结构中,以便我们能够轻松地处理(并稍后增强)。

因此,如果我们有“/product/update/7/notify”,我们希望我们的语法能够提供一个匹配对象,该对象具有“product”的subject、“update”的command和“7/notify”的data

我们将从定义一个语法类和一些用于主题、命令和数据的匹配方法开始。我们将使用 token 声明器,因为我们不关心空格。

grammar REST {
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
}

到目前为止,这个 REST 语法表示我们想要一个仅包含单词字符的主题,一个仅包含单词字符的命令,以及字符串中剩下的所有其他内容作为数据。

接下来,我们希望在 URI 的更大上下文中排列这些匹配的 token。这就是 TOP 方法允许我们做的事情。我们将添加 TOP 方法,并将我们 token 的名称放在其中,以及构成整体模式的其余模式。注意我们是如何从命名的正则表达式构建更大的正则表达式的。

grammar REST {
    token TOP     { '/' <subject> '/' <command> '/' <data> }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
}

有了这段代码,我们已经可以获取 RESTful 请求的三个部分了

my $match = REST.parse('/product/update/7/notify');
say $match;
 
# OUTPUT: «「/product/update/7/notify」␤ 
#          subject => 「product」 
#          command => 「update」 
#          data => 「7/notify」» 

可以通过使用$match<subject>$match<command>$match<data>来直接访问数据,以返回解析的值。它们都包含可以进一步处理的匹配对象,例如强制转换为字符串($match<command>.Str)。

添加一些灵活性§

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

因此,现在我们有

grammar REST {
    token TOP     { '/' <subject> '/' <command> [ '/' <data> ]? }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
}
 
my $m = REST.parse('/product/create');
say $m<subject>$m<command>;
 
# OUTPUT: «「product」「create」␤» 

接下来,假设 URI 将由用户手动输入,并且用户可能会不小心在“/”之间添加空格。如果我们想容纳这种情况,我们可以用允许空格的 token 替换 TOP 中的“/”。

grammar REST {
    token TOP     { <slash><subject><slash><command>[<slash><data>]? }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
 
    token slash   { \s* '/' \s* }
}
 
my $m = REST.parse('/ product / update /7 /notify');
say $m;
 
# OUTPUT: «「/ product / update /7 /notify」␤ 
#          slash => 「/ 」 
#          subject => 「product」 
#          slash => 「 / 」 
#          command => 「update」 
#          slash => 「 /」 
#          data => 「7 /notify」» 

现在我们在匹配对象中获得了一些额外的垃圾,包括那些斜杠。有一些技术可以清理这些垃圾,我们将在后面介绍。

从语法继承§

由于语法是类,因此它们在 OOP 方面与任何其他类一样;具体来说,它们可以从包含一些 token 或规则的基类继承,这样

grammar Letters {
    token letters { \w+ }
}
 
grammar Quote-Quotes {
    token quote { "\"" | "`" | "'" }
}
 
grammar Quote-Other {
    token quote { "|" | "/" | "¡" }
}
 
grammar Quoted-Quotes is Letters is Quote-Quotes {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }
}
 
grammar Quoted-Other is Letters is Quote-Other {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }
}
 
my $quoted = q{"enhanced"};
my $parsed = Quoted-Quotes.parse($quoted);
say $parsed;
# OUTPUT: 
#「"enhanced"」 
# quote => 「"」 
# letters => 「enhanced」 
#quote => 「"」 
 
$quoted = "|barred|";
$parsed = Quoted-Other.parse($quoted);
say $parsed;
# OUTPUT: 
#|barred|」 
#quote => 「|」 
#letters => 「barred」 
#quote => 「|」 

此示例使用多重继承通过改变对应于quotes的规则来组合两种不同的语法。在这种情况下,除了,我们更倾向于使用组合而不是继承,因此我们可以使用角色而不是继承。

role Letters {
    token letters { \w+ }
}
 
role Quote-Quotes {
    token quote { "\"" | "`" | "'" }
}
 
role Quote-Other {
    token quote { "|" | "/" | "¡" }
}
 
grammar Quoted-Quotes does Letters does Quote-Quotes {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }
}
 
grammar Quoted-Other does Letters does Quote-Other {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }
 
}

将输出与上面的代码完全相同。作为类和角色之间差异的症状,像使用角色组合两次定义token quote这样的冲突会导致错误

grammar Quoted-Quotes does Letters does Quote-Quotes does Quote-Other { ... }
# OUTPUT: ... Error while compiling ... Method 'quote' must be resolved ... 

添加一些约束§

我们希望我们的 RESTful 语法只允许 CRUD 操作。我们希望任何其他操作都无法解析。这意味着上面的“命令”应该具有四个值之一:创建、检索、更新或删除。

有几种方法可以实现这一点。例如,您可以更改命令方法

token command { \w+ }
 
# …becomes… 
 
token command { 'create'|'retrieve'|'update'|'delete' }

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

还有另一种技术可以提供更大的灵活性,并在选项增多时提高可读性:原型正则表达式

为了利用这些原型正则表达式(实际上是多方法)来限制我们自己使用有效的 CRUD 选项,我们将用以下内容替换 token command

proto token command {*}
token command:sym<create>   { <sym> }
token command:sym<retrieve> { <sym> }
token command:sym<update>   { <sym> }
token command:sym<delete>   { <sym> }

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

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

如果某个原型正则表达式选项的匹配条件发生,则整个原型的搜索将终止。匹配的数据将以匹配对象的格式分配给父原型令牌。如果使用了特殊 <sym> 令牌并构成实际匹配的全部或部分,则它将作为匹配对象中的子级保留,否则它将不存在。

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

将我们的 RESTful 语法组合在一起§

这是我们到目前为止用于处理 RESTful URI 的内容

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }
 
    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }
 
    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}

让我们看看各种 URI 以及它们如何与我们的语法一起工作。

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

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

仅使用语法的一部分,我们几乎获得了我们想要的一切。URI 被解析,我们得到一个包含数据的结构。

data 令牌将 URI 的整个末尾作为单个字符串返回。4 是可以的。但是,从“7/notify”中,我们只想要 7。为了只获取 7,我们将使用语法类的另一个功能:操作

语法操作§

语法操作在语法类中使用,用于对匹配项执行操作。操作在它们自己的类中定义,与语法类不同。

您可以将语法操作视为语法的一种插件扩展模块。很多时候,您会很乐意使用语法本身。但是,当您需要进一步处理其中一些字符串时,您可以插入 Actions 扩展模块。

要使用操作,您使用一个名为 actions 的命名参数,该参数应该包含您的操作类的实例。使用上面的代码,如果我们的操作类名为 REST-actions,我们将像这样解析 URI 字符串

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

如果您使用与语法方法(令牌、正则表达式、规则)相同的名称命名您的操作方法,那么当您的语法方法匹配时,您的操作方法(具有相同的名称)将被自动调用。该方法还将传递相应的匹配对象(由 $/ 变量表示)。

让我们来看一个例子。

带有操作的语法示例§

我们现在回到我们的语法。

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }
 
    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }
 
    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}

回想一下,我们想要进一步处理数据令牌“7/notify”,以获取 7。为此,我们将创建一个操作类,该类具有与命名令牌同名的一个方法。在本例中,我们的令牌名为 data,因此我们的方法也名为 data

class REST-actions
{
    method data($/{ $/.split('/'}
}

现在,当我们通过语法传递 URI 字符串时,data 令牌匹配将传递给REST-actions 的 data 方法。操作方法将按“/”字符拆分字符串,返回列表的第一个元素将是 ID 号(对于“7/notify”,为 7)。

但实际上并非如此;还有更多内容。

使用 makemade 使带有操作的语法保持整洁§

如果语法在数据上调用上面的操作,则将调用 data 方法,但不会在返回到我们程序的大型 TOP 语法匹配结果中显示任何内容。为了使操作结果显示出来,我们需要对该结果调用 make。结果可以是多种东西,包括字符串、数组或哈希结构。

你可以想象 make 将结果放置在一个语法专用的容器区域中。我们 make 的所有内容都可以通过 made 在以后访问。

所以,我们应该用以下代码代替上面的 REST-actions 类

class REST-actions
{
    method data($/{ make $/.split('/'}
}

当我们在匹配拆分(返回一个列表)中添加 make 时,该操作将返回一个数据结构到语法中,该数据结构将与原始语法的 data 令牌分开存储。这样,如果需要,我们可以同时使用两者。

如果我们只想访问那个长 URI 中的 ID 7,我们可以访问我们 madedata 操作返回的列表的第一个元素。

my $uri = '/product/update/7/notify';
 
my $match = REST.parse($uriactions => REST-actions.new);
 
say $match<data>.made[0];  # OUTPUT: «7␤» 
say $match<command>.Str;   # OUTPUT: «update␤» 

在这里,我们在数据上调用 made,因为我们想要我们 made(使用 make)的操作结果来获取拆分数组。这很棒!但是,如果我们可以 make 一个更友好的数据结构,其中包含我们想要的所有内容,而不是必须强制类型并记住数组,那岂不是更棒吗?

就像语法的 TOP 匹配整个字符串一样,操作也有一个 TOP 方法。我们可以 make 所有单独的匹配组件,比如 datasubjectcommand,然后我们可以将它们放在一个我们将在 TOP 中 make 的数据结构中。当我们返回最终的匹配对象时,我们就可以访问这个数据结构。

为此,我们在操作类中添加了 TOP 方法,并 make 我们喜欢的任何数据结构,从组件部分开始。

所以,我们的操作类变成了

class REST-actions
{
    method TOP ($/{
        make { subject => $<subject>.Str,
               command => $<command>.Str,
               data    => $<data>.made }
    }
 
    method data($/{ make $/.split('/'}
}

这里在 TOP 方法中,subject 保持与我们在语法中匹配的主题相同。此外,command 返回匹配的有效 <sym>(创建、更新、检索或删除)。我们也将其强制转换为 .Str,因为我们不需要完整的匹配对象。

我们想要确保在 $<data> 对象上使用 made 方法,因为我们想要访问我们在操作中使用 make made 的拆分对象,而不是正确的 $<data> 对象。

在我们 make 了语法操作的 TOP 方法中的某些内容之后,我们就可以通过在语法结果对象上调用 made 方法来访问所有自定义值。代码现在变成了

my $uri = '/product/update/7/notify';
 
my $match = REST.parse($uriactions => REST-actions.new);
 
my $rest = $match.made;
say $rest<data>[0];   # OUTPUT: «7␤» 
say $rest<command>;   # OUTPUT: «update␤» 
say $rest<subject>;   # OUTPUT: «product␤» 

如果不需要完整的返回匹配对象,你可以只从操作的 TOP 返回 made 的数据。

my $uri = '/product/update/7/notify';
 
my $rest = REST.parse($uriactions => REST-actions.new).made;
 
say $rest<data>[0];   # OUTPUT: «7␤» 
say $rest<command>;   # OUTPUT: «update␤» 
say $rest<subject>;   # OUTPUT: «product␤» 

哦,我们忘记去掉那个难看的数组元素编号了吗?嗯。让我们在语法的自定义返回中 TOP 中创建一些新东西... 我们不妨称之为 subject-id,并将其设置为 <data> 的元素 0。

class REST-actions
{
    method TOP ($/{
        make { subject    => $<subject>.Str,
               command    => $<command>.Str,
               data       => $<data>.made,
               subject-id => $<data>.made[0}
    }
 
    method data($/{ make $/.split('/'}
}

现在我们可以这样做

my $uri = '/product/update/7/notify';
 
my $rest = REST.parse($uriactions => REST-actions.new).made;
 
say $rest<command>;    # OUTPUT: «update␤» 
say $rest<subject>;    # OUTPUT: «product␤» 
say $rest<subject-id># OUTPUT: «7␤» 

这是最终代码

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }
 
    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }
 
    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}
 
 
class REST-actions
{
    method TOP ($/{
        make { subject    => $<subject>.Str,
               command    => $<command>.Str,
               data       => $<data>.made,
               subject-id => $<data>.made[0}
    }
 
    method data($/{ make $/.split('/'}
}

直接添加操作§

上面我们看到了如何将语法与操作对象关联起来,并在匹配对象上执行操作。但是,当我们想要处理匹配对象时,这不是唯一的办法。请看下面的例子

grammar G {
  rule TOP { <function-define> }
  rule function-define {
    'sub' <identifier>
    {
      say "func " ~ $<identifier>.made;
      make $<identifier>.made;
    }
    '(' <parameter> ')' '{' '}'
    { say "end " ~ $/.made}
  }
  token identifier { \w+ { make ~$/} }
  token parameter { \w+ { say "param " ~ $/} }
}
 
G.parse('sub f ( a ) { }');
# OUTPUT: «func f␤param a␤end f␤» 

这个例子是解析器的一部分。让我们更多地关注它所展示的功能。

首先,我们可以在语法本身中添加操作,并且这些操作将在正则表达式的控制流到达它们时执行。请注意,操作对象的 method 始终会在整个正则表达式项匹配之后执行。其次,它展示了 make 实际上做了什么,它不过是 $/.made = ... 的语法糖。这个技巧引入了一种从正则表达式项内部传递消息的方法。

希望这能帮助你了解 Raku 中的语法,并向你展示语法和语法操作类是如何协同工作的。有关更多信息,请查看更高级的 Raku 语法指南

有关更多语法调试信息,请参阅 Grammar::Debugger。它为你的每个语法令牌提供断点和彩色编码的 MATCH 和 FAIL 输出。