如何在Perl 6中创建语法
在编程中,语法是一组解析文本的规则。它们非常有用,例如,您可以使用语法来检查一个文本字符串是否符合特定的标准。Perl 6原生支持语法——它们非常容易编写,一旦开始使用,您会发现自己在各个地方都在使用它们。
最近我一直在工作于Module::Minter,这是一个用于创建新Perl 6模块基本骨架结构的简单应用程序。我需要一个方法来检查建议的模块名称是否符合Perl 6的命名约定。
模块名称可以描述为由两个冒号分隔的标识符,例如File::Compare。标识符必须以字母字符(a-z)或下划线开头,后跟零个或多个字母数字字符。到目前为止,一切顺利,但并不简单;一些模块名称只有一个标识符而没有冒号,例如Bailador,而其他模块则更复杂,例如HTTP::Server::Async::Plugins::Router::Simple。这似乎是使用语法的合适工作!
定义语法
Perl 6语法由正则表达式组成。我需要两个正则表达式:一个用于匹配标识符,一个用于匹配双冒号分隔符。对于标识符正则表达式,我将使用
<[A..Za..z_]> # begins with letter or underscore
<[A..Za..z0..9]> ** 0..* # zero or more alpanumeric
记住我们正在使用Perl 6正则表达式,如果您习惯了Perl 5风格的正则表达式,那么这些内容可能会有所不同。字符类由<[ ... ]>
定义,范围使用范围运算符..
定义,而不是连字符。这个正则表达式匹配任何以字母或下划线开头的字符,后跟零个或多个字母数字字符。匹配两个冒号很简单
\:\: # colon pairs
使用grammar
关键字定义语法,后跟语法的名称。我将把这个语法命名为Legal::Module::Name
grammar Legal::Module::Name
{
...
}
现在我可以将正则表达式作为令牌添加到语法中
grammar Legal::Module::Name
{
token identifier
{
# leading alpha or _ only
<[A..Za..z_]>
<[A..Za..z0..9]> ** 0..*
}
token separator
{
\:\: # colon pairs
}
}
每个语法都需要一个名为TOP
的令牌,这是语法的起点
grammar Legal::Module::Name
{
token TOP
{ # identifier followed by zero or more separator identifier pairs
^ <identifier> [<separator><identifier>] ** 0..* $
}
token identifier
{
# leading alpha or _ only
<[A..Za..z_]>
<[A..Za..z0..9]> ** 0..*
}
token separator
{
\:\: # colon pairs
}
}
TOP
令牌定义一个有效的模块名称,它以标识符令牌开头,后跟零个或多个分隔符和标识符令牌对。这既易于编写也易于维护——假设我想更改分隔符的规则以包括连字符(‘-’),我只需更新分隔符令牌的正则表达式,效果就会自动传播到TOP
令牌定义。
使用语法
现在我已经有了语法,是时候将其付诸实践。parse
方法在字符串上运行语法,如果成功,则返回一个匹配对象。此代码解析$proposed_module_name
字符串,如果提议的模块名称无效,则打印出错误消息或匹配对象。
my $proposed_module_name = 'Super::New::Module';
my $match_obj = Legal::Module::Name.parse($proposed_module_name);
if $match_obj
{
say $match_obj;
}
else
{
say 'Invalid module name!';
}
此代码打印
「Super::New::Module」
identifier => 「Super」
separator => 「::」
identifier => 「New」
separator => 「::」
identifier => 「Module」
从匹配对象中提取内容
我们不仅可以向命令行中输出匹配对象的全部内容,还可以从匹配对象中提取匹配的令牌。这使用的是Perl 6中常用到的引号语法(例如命名正则表达式和散列键)
say $match_obj[0].Str; # Super
say $match_obj[1].Str; # New
say $match_obj[2].Str; # Module
say $match_obj; # all 3 captures
动作类
到目前为止,语法可以检测提议的模块名称是否合法,并从匹配对象中轻松提取模块名称的各个部分。Perl 6还允许您添加一个动作类,该类定义了匹配令牌的额外行为。我想添加一个警告,当模块名称具有过多的标识符时,换句话说,它是一个合法的模块名称,但用户可能想要缩短它。首先,我定义动作类本身
class Module::Name::Actions
{
method TOP($/)
{
if $<identifier>.elems > 5
{
warn 'Module name has a lot of identifiers, consider simplifying the name';
}
}
}
如您所见,这是一个普通的Perl 6类定义。我添加了一个名为TOP
的方法,它匹配语法中的第一个标记。我使用命名正则表达式语法来计算所有标识符匹配的数量,如果有超过5个,则发出警告。这不会阻止代码运行,但可能会使用户重新考虑他们的模块名称选择。
然后我初始化动作类,并将其作为参数传递给parse
。
my $actions = Module::Name::Actions.new;
my $match_obj = Legal-Module-Name.parse($proposed_module_name, :actions($actions));
每当在解析过程中遇到标记时,语法将调用匹配的动作类方法。在这种情况下,每次解析都会调用一次,但我们可以为标识符标记添加额外的长度。查看Module::Minter的源代码,了解如何将语法集成到模块中。
Perl 5中的语法
您也可以在Perl 5中编写语法。对于类似于Perl 6的实现,请查看Regexp::Grammars或Ingy Döt Net的Pegex发行版。对于不同的方法,请查看brian d foy所著的《Mastering Perl》的第一章,其中包含一个JSON语法的示例。
注:这并不完全正确 - 整个名称(包括冒号)都是标识符。
更新: 添加了Regexp::Grammars链接。2015-01-13
本文最初发布在PerlTricks.com上。
标签
反馈
这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。