如何在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上。

标签

David Farrell

David是一位职业程序员,他经常推文博客关于代码和编程艺术。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。