Perl 6 中的定序器是如何工作的

在本系列比较 Perl 5 和 Perl 6 的第六篇文章中,了解 Perl 5 中的特殊块如何转换为 Perl 6 中的定序器。
222 位读者喜欢这篇文章。
Programming keyboard.

Opensource.com

这是一系列关于将代码从 Perl 5 迁移到 Perl 6 的文章中的第六篇。本文着眼于 Perl 5 中的特殊块,例如 BEGINEND,以及所谓的 Perl 6 中的定序器在语义上可能存在的细微变化。

Perl 6 将 Perl 5 的一些特性泛化为定序器,这些特性在 Perl 5 的特殊块中没有涵盖。并且它添加了其他定序器,这些定序器根本没有被任何(标准)Perl 5 功能所涵盖。

关于定序器,要记住的一个重要特性是,它们是程序正常执行流程的一部分。运行时执行器根据定序器的类型和上下文来决定何时运行定序器。因此,Perl 6 中的所有定序器都使用大写字符拼写,使其突出显示。

概述

让我们从 Perl 5 的特殊块及其在 Perl 6 中的对应物概述开始,按照它们在 Perl 5 中执行的顺序排列

Perl 5 Perl 6 注释
BEGIN BEGIN BEGIN
UNITCHECK CHECK  
CHECK   CHECK
INIT INIT  
INIT INIT  

END

BEGIN

END

Perl 6 中的这些定序器通常被称为 程序执行定序器 ,因为它们与完整程序的执行相关,而与它们在程序中的位置无关。

Perl 5 中 BEGIN 特殊块和 Perl 6 中 BEGIN 定序器的语义是相同的。它指定了一段代码,一旦解析完成(即程序,又名编译单元,作为一个整体被解析之前),就立即执行。

然而,在 Perl 6 中使用 BEGIN 存在一个问题:Perl 6 中的模块默认是预编译的,这与 Perl 5 不同,Perl 5 没有任何模块或脚本的预编译。

作为 Perl 6 模块的用户或开发者,您不必考虑模块是否应该(再次)预编译。这在安装模块时以及每次 Rakudo Perl 6 更新后都会在后台自动完成。当开发者对模块进行更改时,也会自动完成。您可能注意到的唯一事情是在加载模块时会有轻微延迟。

# Perl 5
my $DEBUG;
BEGIN { $DEBUG = $ENV{DEBUG} // 0 }

这意味着 BEGIN 块仅在发生预编译时执行,不是每次加载模块时都执行。这与 Perl 5 不同,在 Perl 5 中,模块通常仅以源代码形式存在,每次加载模块时都会编译(即使该模块可以加载已编译的本地库组件)。

当将代码从 Perl 5 移植到 Perl 6 时,这可能会引起一些不愉快的意外,因为预编译可能发生在很久以前,甚至在不同的机器上(如果是从操作系统分发的软件包安装的)。考虑使用环境变量的值来启用调试的情况。在 Perl 5 中,您可以这样写

# Perl 6
no precompilation;  # this code should not be pre-compiled

这在 Perl 5 中可以正常工作,因为每次加载模块时都会编译模块,因此 BEGIN 块每次加载模块时都会运行。并且 $DEBUG 的值将是正确的,具体取决于环境变量的设置。但在 Perl 6 中却不是这样。因为 BEGIN 定序器只执行一次,即在预编译时执行,所以 $DEBUG 变量的值将在模块预编译时确定,而不是在模块加载时确定!

  • 一个简单的解决方法是阻止 Perl 6 模块的预编译

  • 但是,预编译有几个优点,您不想轻易忽略

数据结构设置只需完成一次。如果您有必须在每次加载模块时设置的数据结构,您可以在模块预编译时完成一次。如果模块经常加载,这可能会节省大量时间和 CPU。

# Perl 5
use constant DEBUG => $ENV{DEBUG} // 0;
# Perl 6
my constant DEBUG = %*ENV<DEBUG> // 0;

它可以更快地加载模块。由于它不需要解析任何源代码,因此预编译模块的加载速度比一遍又一遍编译的模块快得多。一个典型的例子是 Perl 6 的核心设置——用 Perl 6 编写的部分。这由一个 64 KLOC/2MB 的源文件组成(从许多单独的源文件生成以实现可维护性)。在 Perl 6 安装期间编译此源文件大约需要一分钟。在 Perl 6 启动时加载此预编译代码大约需要 125 毫秒。这几乎是 500 倍 的速度提升!

# Perl 6
INIT my \DEBUG = %*ENV<DEBUG> // 0;  # sigilless variable bound to value

Perl 5 Perl 6 的其他一些隐式使用 BEGIN 功能的特性也存在相同的警告。以下示例中,我们希望常量 DEBUG 具有环境变量 DEBUG 的值,或者,如果不可用,则值为 0

# Perl 6
say "This module was compiled at { BEGIN DateTime.now }";
# This module was compiled at 2018-10-04T22:18:39.598087+02:00

Perl 6 中最好的等价物可能是 INIT 定序器

UNITCHECK

与 Perl 5 中一样,INIT 定序器在执行开始之前运行。您还可以将 Perl 6 的模块预编译行为用作一项功能

CHECK

但更多关于该语法的将在后面介绍。

INIT

Perl 5 中 UNITCHECK 特殊块的功能由 Perl 6 中的 CHECK 定序器执行。否则,它是相同的;它指定了一段代码,在当前编译单元的编译完成时执行。

Perl 6 中没有 Perl 5 CHECK 特殊块的等价物。主要原因是您可能不应该再在 Perl 5 中使用 CHECK 特殊块了;而是使用 UNITCHECK ,因为它的语义更合理。(自 5.10 版本以来可用。)

INIT

Perl 6 中 INIT 定序器的功能与 Perl 5 中 INIT 特殊块相同。它指定了一段代码,在编译单元中的代码执行之前执行。

在 Perl 6 的预编译模块中,INIT 定序器可以用作 BEGIN 定序器的替代方案。

Perl 6 中 END 定序器的功能与 Perl 5 中 END 特殊块相同。它指定了一段代码,在编译单元中的所有代码执行之后执行,或者在代码决定退出时执行(无论是预期的还是意外的,因为抛出了异常)。

# Perl 5
say "running in Perl 5";
END       { say "END"   }
INIT      { say "INIT"  }
UNITCHECK { say "CHECK" }
BEGIN     { say "BEGIN" }
# BEGIN
# CHECK
# INIT
# running in Perl 5
# END

# Perl 6
say "running in Perl 6";
END   { say "END"   }
INIT  { say "INIT"  }
CHECK { say "CHECK" }
BEGIN { say "BEGIN" }
# BEGIN
# CHECK
# INIT
# running in Perl 6
# END

示例

这是一个示例,使用了所有四个程序执行定序器及其 Perl 5 特殊块对应物

不仅仅是特殊块

Perl 6 中的定序器具有额外的功能,使其不仅仅是特殊块。

# Perl 5
# need to define lexical outside of BEGIN scope
my $foo;
# otherwise it won't be known in the rest of the code
BEGIN { $foo = %*ENV<FOO> // 42 };

不需要块

# Perl 6
# share scope with surrounding code
BEGIN my $foo = %*ENV<FOO> // 42;

Perl 6 中的大多数定序器不必是 (即,后面跟着花括号之间的代码)。它们也可以由单个语句组成,没有任何花括号。这意味着,如果您在 Perl 5 中编写了以下内容

您可以在 Perl 6 中将其写为

# Perl 6
my $foo = BEGIN %*ENV<FOO> // 42;

可以返回值

所有程序执行定序器都返回其代码的最后一个值,以便您可以在表达式中使用它们。上面使用 BEGIN 的示例也可以写成

# Perl 6
my $foo = INIT %*ENV<FOO> // 42;

当与 BEGIN 定序器一起使用时,您正在创建一个匿名常量并在运行时分配它。

由于模块预编译,如果您想在模块中进行这种类型的初始化,您最好使用 INIT 定序器

这确保了该值将在模块加载时而不是在预编译时(通常在模块安装期间发生一次)确定。

Perl 6 中的其他定序器

如果您只对了解 Perl 5 特殊块在 Perl 6 中如何工作感兴趣,您可以跳过本文的其余部分。但您将错过人们已实现的一些非常好的和有用的功能。

块和循环定序器

块和循环定序器始终与周围的块相关联,无论它们位于块中的哪个位置。除了您不限于只有一个,尽管您可以争辩说拥有多个并不能提高可维护性。 请注意,任何 submethod 在这些定序器方面被视为块。
名称  描述
ENTER 每次进入块时运行
LEAVE 每次离开块时运行
PRE 在运行块之前检查条件
POST 在运行块后检查返回值
KEEP 每次成功离开块时运行

UNDO

每次成功离开块时运行

# Perl 6
say "outside";
{
    LEAVE say "left";
    ENTER say "entered";
    say "inside";
}
say "outside again";
# outside
# entered
# inside
# left
# outside again

ENTER & LEAVE

# Perl 6
{
    LEAVE say "stayed " ~ (now - ENTER now) ~ " seconds";
    sleep 2;
}
# stayed 2.001867 seconds

ENTERLEAVE 定序器是不言自明的:当进入块时,调用 ENTER 定序器。当离开块时(无论是正常离开还是通过异常离开),调用 LEAVE 定序器。一个简单的例子

ENTER 定序器的最后一个值被返回 ,以便可以在表达式中使用它。这是一个有点人为的例子

LEAVE 定序器对应于许多其他现代编程语言中的 DEFER 功能。

KEEP & UNDO

# Perl 6
for 42, Nil {
    KEEP { say "Keeping because of $_" }
    UNDO { say "Undoing because of $_.perl()" }
    $_;
}
# Keeping because of 42
# Undoing because of Nil

KEEPUNDO 定序器是 LEAVE 定序器的特殊情况。它们根据周围块的返回值调用。如果对返回值调用 defined 方法的结果为 True,则将调用任何 KEEP 定序器。如果调用 defined 的结果不是 True,则将调用任何 UNDO 定序器。块的实际值将在主题中可用(即 $_)。

# Perl 6
{
    KEEP $dbh.commit;
    UNDO $dbh.rollback;
    ...    # set up a big transaction in a database
    True;  # indicate success
}

一个虚构的例子可能会澄清

一个真实生活中的例子也可能澄清

因此,如果设置数据库中的大型事务出现任何问题,UNDO 定序器会确保事务可以回滚。相反,如果块成功离开,事务将由 KEEP 定序器自动提交。

KEEPUNDO 定序器为您提供了简易版 软件事务内存 的构建块。

PRE & POST

PRE 定序器是 ENTER 定序器的特殊版本。POST 定序器是 LEAVE 定序器的特殊情况。

# Perl 6
{
    PRE { say "called PRE"; False }    # throws exception
    ...
}
say "we made it!";                     # never makes it here
# called PRE
# Precondition '{ say "called PRE"; False }' failed

# Perl 6
{
    PRE  { say "called PRE"; True   }  # does NOT throw exception
    POST { say "called POST"; False }  # throws exception
    say "inside the block";            # also returns True
}
say "we made it!";                     # never makes it here
# called PRE
# inside the block
# called POST
# Postcondition '{ say "called POST"; False }' failed

如果可以进入块,则 PRE 定序器应返回真值。如果不是,则会抛出异常。POST 定序器接收块的返回值,并且如果可以离开块而不抛出异常,则应返回真值。

# Perl 6
{
    POST { $_ ~~ Int }   # check if the return value is an Int
    ...                  # calculate result
    $result;
}

一些例子

# Perl 6
--> Int {                # return value should be an Int
    ...                  # calculate result
    $result;
}

如果您只想检查块是否返回特定值或类型,您最好为块指定返回签名。请注意

只是一种非常迂回的方式来表达

通常,只有当必要的检查非常复杂且无法简化为简单的类型检查时,您才会使用 POST 定序器。

循环定序器 循环定序器是特定于循环结构的块定序器的特殊类型。一个在第一次迭代之前运行(FIRST),一个在每次迭代之后运行(NEXT),一个在最后一次迭代之后运行(LAST)。
名称 描述 
FIRST 在第一次迭代之前运行
NEXT 在每次完成的迭代或使用 next 时运行

LAST

# Perl 6
my $total = 0;
for 1..5 {
    $total += $_;
    LAST  say "------ +\n$total.fmt('%6d')";
    FIRST say "values\n======";
    NEXT  say .fmt('%6d');
}
# values
# ======
#      1
#      2
#      3
#      4
#      5
# ------ +
#     15

在最后一次迭代或使用 last 时运行

名称不言自明。一个有点人为的例子

循环结构包括 loopwhile、untilrepeat/whilerepeat/untilfor;以及 mapdeepmapflatmap

如果需要,您可以将循环定序器与其他块定序器一起使用,但这通常是不必要的。

除了在 Perl 6 中有对应物的 Perl 5 特殊块(称为定序器)之外,Perl 6 还有许多与代码块和循环结构相关的专用定序器。Perl 6 还有与异常处理和警告、事件驱动编程以及文档 (pod) 解析相关的定序器;这些将在本系列的后续文章中介绍。

Elizabeth Mattijsen 自 1978 年以来一直从事编程工作,使用过各种(现在大多已过时的)编程语言,之后她开始使用 Perl 4 编程。1994 年,她创立了荷兰第一家商业网站开发公司,使用 Perl 5 作为主要编程语言。从 2003 年起,她参与了一项在线酒店预订服务的快速增长。

Perl 正在走向灭绝吗?
© . All rights reserved.