开始之前§
为什么使用语法?§
语法分析字符串并从这些字符串中返回数据结构。语法可用于准备程序执行,确定程序是否可以运行(如果它是一个有效的程序),将网页分解为组成部分,或识别句子的不同部分,等等。
我什么时候会使用语法?§
如果您需要驯服或解释字符串,语法提供了完成此工作的工具。
该字符串可以是您要分解成部分的文件;也许是一个协议,比如 SMTP,您需要指定哪些“命令”在哪些用户提供的数据之后;也许您正在设计自己的领域特定语言。语法可以提供帮助。
语法的广义概念§
正则表达式 (Regexes) 非常适合在字符串中查找模式。但是,对于某些任务,例如一次查找多个模式,或组合模式,或测试可能围绕字符串的模式,仅使用正则表达式是不够的。
在处理 HTML 时,您可以定义一个语法来识别 HTML 标签,包括开始和结束元素,以及它们之间的文本。然后,您可以将这些元素组织成数据结构,例如数组或哈希表。
深入技术§
概念概述§
语法是一种特殊的类。您可以像声明和定义任何其他类一样声明和定义语法,只是您使用grammar关键字而不是class关键字。
作为这样的类,语法由定义正则表达式、标记或规则的方法组成。这些都是不同类型匹配方法的变体。定义语法后,您可以调用它并将字符串传递给它进行解析。
my = G.parse();
现在,您可能想知道,如果我定义了所有这些正则表达式,它们只返回结果,那么这如何帮助解析可能在另一个字符串中向前或向后,或者需要从这些正则表达式中组合起来的东西呢?... 这就是语法操作发挥作用的地方。
对于您在语法中匹配的每个“方法”,您都会获得一个操作,您可以使用它来对该匹配进行操作。您还会获得一个总体操作,您可以使用它来将所有匹配项绑定在一起并构建数据结构。默认情况下,此总体方法称为TOP
。
技术概述§
如前所述,语法使用grammar关键字声明,其“方法”使用regex、token或rule声明。
正则表达式方法速度慢但彻底,它们会回溯字符串并真正尝试。
标记方法比正则表达式方法快,并且忽略空格。标记方法不会回溯;它们在第一次可能的匹配后放弃。
规则方法与标记方法相同,只是不忽略空格。
当方法(正则表达式、标记或规则)在语法中匹配时,匹配的字符串将放入一个匹配对象中,并使用与方法相同的名称作为键。
如果您要使用my $match = G.parse($string)
,并且您的字符串以 'clever_text_keyword' 开头,您将获得一个匹配对象,其中包含 'clever_text_keyword',并使用您匹配对象中<thingy>
的名称作为键。例如
my = G.parse("Þor is mighty");say .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 声明器,因为我们不关心空格。
到目前为止,这个 REST 语法表示我们想要一个仅包含单词字符的主题,一个仅包含单词字符的命令,以及字符串中剩下的所有其他内容作为数据。
接下来,我们希望在 URI 的更大上下文中排列这些匹配的 token。这就是 TOP 方法允许我们做的事情。我们将添加 TOP 方法,并将我们 token 的名称放在其中,以及构成整体模式的其余模式。注意我们是如何从命名的正则表达式构建更大的正则表达式的。
有了这段代码,我们已经可以获取 RESTful 请求的三个部分了
my = REST.parse('/product/update/7/notify');say ;# OUTPUT: «「/product/update/7/notify」# subject => 「product」# command => 「update」# data => 「7/notify」»
可以通过使用$match<subject>
或$match<command>
或$match<data>
来直接访问数据,以返回解析的值。它们都包含可以进一步处理的匹配对象,例如强制转换为字符串($match<command>.Str
)。
添加一些灵活性§
到目前为止,语法可以处理检索、删除和更新。但是,创建命令没有第三部分(数据部分)。这意味着如果我们尝试解析创建 URI,语法将无法匹配。为了避免这种情况,我们需要使最后一个数据位置匹配可选,以及它前面的“/”。这可以通过在 TOP token 的分组“/”和数据组件中添加一个问号来实现,以指示它们的可选性质,就像普通的正则表达式一样。
因此,现在我们有
my = REST.parse('/product/create');say <subject>, <command>;# OUTPUT: «「product」「create」»
接下来,假设 URI 将由用户手动输入,并且用户可能会不小心在“/”之间添加空格。如果我们想容纳这种情况,我们可以用允许空格的 token 替换 TOP 中的“/”。
my = REST.parse('/ product / update /7 /notify');say ;# OUTPUT: «「/ product / update /7 /notify」# slash => 「/ 」# subject => 「product」# slash => 「 / 」# command => 「update」# slash => 「 /」# data => 「7 /notify」»
现在我们在匹配对象中获得了一些额外的垃圾,包括那些斜杠。有一些技术可以清理这些垃圾,我们将在后面介绍。
从语法继承§
由于语法是类,因此它们在 OOP 方面与任何其他类一样;具体来说,它们可以从包含一些 token 或规则的基类继承,这样
is Letters is Quote-Quotesis Letters is Quote-Othermy = q{"enhanced"};my = Quoted-Quotes.parse();say ;# OUTPUT:#「"enhanced"」# quote => 「"」# letters => 「enhanced」#quote => 「"」= "|barred|";= Quoted-Other.parse();say ;# OUTPUT:#quote => 「|」#letters => 「barred」#quote => 「|」
此示例使用多重继承通过改变对应于quotes
的规则来组合两种不同的语法。在这种情况下,除了,我们更倾向于使用组合而不是继承,因此我们可以使用角色而不是继承。
does Letters does Quote-Quotesdoes Letters does Quote-Other
将输出与上面的代码完全相同。作为类和角色之间差异的症状,像使用角色组合两次定义token quote
这样的冲突会导致错误
does Letters does Quote-Quotes does Quote-Other# OUTPUT: ... Error while compiling ... Method 'quote' must be resolved ...
添加一些约束§
我们希望我们的 RESTful 语法只允许 CRUD 操作。我们希望任何其他操作都无法解析。这意味着上面的“命令”应该具有四个值之一:创建、检索、更新或删除。
有几种方法可以实现这一点。例如,您可以更改命令方法
# …becomes…
为了使 URI 成功解析,字符串中“/”之间的第二部分必须是这些 CRUD 值之一,否则解析将失败。这正是我们想要的。
还有另一种技术可以提供更大的灵活性,并在选项增多时提高可读性:原型正则表达式。
为了利用这些原型正则表达式(实际上是多方法)来限制我们自己使用有效的 CRUD 选项,我们将用以下内容替换 token command
proto
sym
关键字用于创建各种原型正则表达式选项。每个选项都有一个名称(例如,sym<update>
),并且为了使用该选项,会自动生成一个具有相同名称的特殊 <sym>
令牌。
<sym>
令牌以及其他用户定义的令牌可以在原型正则表达式选项块中使用,以定义特定的匹配条件。正则表达式令牌是编译后的形式,一旦定义,就不能被副词操作(例如,:i
)修改。因此,由于它是自动生成的,特殊 <sym>
令牌仅在需要精确匹配选项名称时有用。
如果某个原型正则表达式选项的匹配条件发生,则整个原型的搜索将终止。匹配的数据将以匹配对象的格式分配给父原型令牌。如果使用了特殊 <sym>
令牌并构成实际匹配的全部或部分,则它将作为匹配对象中的子级保留,否则它将不存在。
像这样使用原型正则表达式给了我们很大的灵活性。例如,我们可以输入我们自己的字符串,或者做其他有趣的事情,而不是返回 <sym>
(在本例中是匹配的整个字符串)。我们可以对 token subject
方法做同样的事情,并将其限制为仅在有效主题(如“part”或“people”等)上正确解析。
将我们的 RESTful 语法组合在一起§
这是我们到目前为止用于处理 RESTful URI 的内容
让我们看看各种 URI 以及它们如何与我们的语法一起工作。
my = ['/product/update/7/notify','/product/create','/item/delete/4'];for -># 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 = REST.parse(, actions => REST-actions.new);# …or if you prefer…my = REST.parse(, :actions(REST-actions.new));
如果您使用与语法方法(令牌、正则表达式、规则)相同的名称命名您的操作方法,那么当您的语法方法匹配时,您的操作方法(具有相同的名称)将被自动调用。该方法还将传递相应的匹配对象(由 $/
变量表示)。
让我们来看一个例子。
带有操作的语法示例§
我们现在回到我们的语法。
回想一下,我们想要进一步处理数据令牌“7/notify”,以获取 7。为此,我们将创建一个操作类,该类具有与命名令牌同名的一个方法。在本例中,我们的令牌名为 data
,因此我们的方法也名为 data
。
现在,当我们通过语法传递 URI 字符串时,data 令牌匹配将传递给REST-actions 的 data 方法。操作方法将按“/”字符拆分字符串,返回列表的第一个元素将是 ID 号(对于“7/notify”,为 7)。
但实际上并非如此;还有更多内容。
使用 make
和 made
使带有操作的语法保持整洁§
如果语法在数据上调用上面的操作,则将调用 data 方法,但不会在返回到我们程序的大型 TOP
语法匹配结果中显示任何内容。为了使操作结果显示出来,我们需要对该结果调用 make。结果可以是多种东西,包括字符串、数组或哈希结构。
你可以想象 make
将结果放置在一个语法专用的容器区域中。我们 make
的所有内容都可以通过 made 在以后访问。
所以,我们应该用以下代码代替上面的 REST-actions 类
当我们在匹配拆分(返回一个列表)中添加 make
时,该操作将返回一个数据结构到语法中,该数据结构将与原始语法的 data
令牌分开存储。这样,如果需要,我们可以同时使用两者。
如果我们只想访问那个长 URI 中的 ID 7,我们可以访问我们 made
的 data
操作返回的列表的第一个元素。
my = '/product/update/7/notify';my = REST.parse(, actions => REST-actions.new);say <data>.made[0]; # OUTPUT: «7»say <command>.Str; # OUTPUT: «update»
在这里,我们在数据上调用 made
,因为我们想要我们 made
(使用 make
)的操作结果来获取拆分数组。这很棒!但是,如果我们可以 make
一个更友好的数据结构,其中包含我们想要的所有内容,而不是必须强制类型并记住数组,那岂不是更棒吗?
就像语法的 TOP
匹配整个字符串一样,操作也有一个 TOP 方法。我们可以 make
所有单独的匹配组件,比如 data
或 subject
或 command
,然后我们可以将它们放在一个我们将在 TOP 中 make
的数据结构中。当我们返回最终的匹配对象时,我们就可以访问这个数据结构。
为此,我们在操作类中添加了 TOP
方法,并 make
我们喜欢的任何数据结构,从组件部分开始。
所以,我们的操作类变成了
这里在 TOP
方法中,subject
保持与我们在语法中匹配的主题相同。此外,command
返回匹配的有效 <sym>
(创建、更新、检索或删除)。我们也将其强制转换为 .Str
,因为我们不需要完整的匹配对象。
我们想要确保在 $<data>
对象上使用 made
方法,因为我们想要访问我们在操作中使用 make
made
的拆分对象,而不是正确的 $<data>
对象。
在我们 make
了语法操作的 TOP
方法中的某些内容之后,我们就可以通过在语法结果对象上调用 made
方法来访问所有自定义值。代码现在变成了
my = '/product/update/7/notify';my = REST.parse(, actions => REST-actions.new);my = .made;say <data>[0]; # OUTPUT: «7»say <command>; # OUTPUT: «update»say <subject>; # OUTPUT: «product»
如果不需要完整的返回匹配对象,你可以只从操作的 TOP
返回 made
的数据。
my = '/product/update/7/notify';my = REST.parse(, actions => REST-actions.new).made;say <data>[0]; # OUTPUT: «7»say <command>; # OUTPUT: «update»say <subject>; # OUTPUT: «product»
哦,我们忘记去掉那个难看的数组元素编号了吗?嗯。让我们在语法的自定义返回中 TOP
中创建一些新东西... 我们不妨称之为 subject-id
,并将其设置为 <data>
的元素 0。
现在我们可以这样做
my = '/product/update/7/notify';my = REST.parse(, actions => REST-actions.new).made;say <command>; # OUTPUT: «update»say <subject>; # OUTPUT: «product»say <subject-id>; # OUTPUT: «7»
这是最终代码
直接添加操作§
上面我们看到了如何将语法与操作对象关联起来,并在匹配对象上执行操作。但是,当我们想要处理匹配对象时,这不是唯一的办法。请看下面的例子
G.parse('sub f ( a ) { }');# OUTPUT: «func fparam aend f»
这个例子是解析器的一部分。让我们更多地关注它所展示的功能。
首先,我们可以在语法本身中添加操作,并且这些操作将在正则表达式的控制流到达它们时执行。请注意,操作对象的 method 始终会在整个正则表达式项匹配之后执行。其次,它展示了 make
实际上做了什么,它不过是 $/.made = ...
的语法糖。这个技巧引入了一种从正则表达式项内部传递消息的方法。
希望这能帮助你了解 Raku 中的语法,并向你展示语法和语法操作类是如何协同工作的。有关更多信息,请查看更高级的 Raku 语法指南。
有关更多语法调试信息,请参阅 Grammar::Debugger。它为你的每个语法令牌提供断点和彩色编码的 MATCH 和 FAIL 输出。