在 Perl 6 中调用子程序和类型

在本系列比较 Perl 5 和 Perl 6 的第九篇文章中,了解子程序的可见性以及(渐进式)类型核心功能中的细微差异。
138 位读者喜欢此文。
Best of Opensource.com: Programming

Flickr 上的 Zagrev. CC BY-SA 2.0

这是关于将代码从 Perl 5 迁移到 Perl 6 的系列的第九篇文章。本文探讨了 Perl 5 和 Perl 6 之间子程序可见性的细微差异以及 Perl 6 的(渐进式)类型核心功能。

本文假设你熟悉签名—如果不是,请在继续之前阅读本系列文章的第四篇Perl 6 中子程序签名如何工作

子程序的可见性

在 Perl 5 中,命名的子程序默认情况下在其定义的的范围内可见,无论定义发生在哪个范围内

# Perl 5
{
    sub foo { "bar" }       # visible outside of this scope
}
say foo();
# bar

在 Perl 6 中,命名的子程序仅在其定义的词法范围内可见

# Perl 6
{
    sub foo() { "bar" }     # only visible in this scope
}
say foo();
# ===SORRY!=== Error while compiling ...
# Undeclared routine:
#     foo used at line ...

请注意,Perl 6 错误消息中的 SORRY! 表示在编译时找不到子程序 foo。这是一个非常有用的功能,可以帮助防止在编写子程序调用时出现子程序名称中的拼写错误。

你可以认为 Perl 6 中的子程序定义总是在前面加上 my,类似于定义词法变量。Perl 5 也有一个(以前是实验性的)词法子程序功能,必须在低于 Perl 5.26 的版本中专门激活

# Perl 5.18 or higher
no warnings 'experimental::lexical_subs';
use feature 'lexical_subs';
{
    my sub foo { "bar" }   # limit visibility to this scope
}
say foo();
# Undefined subroutine &main::foo called at ...

在 Perl 5 和 Perl 6 中,都可以使用 our 作用域指示符作为子程序定义的前缀,但结果略有不同:在 Perl 5 中,这使得子程序在作用域之外可见,但在 Perl 6 中则不然。在 Perl 6 中,子程序的查找始终是词法的:在子程序声明中使用 our (无论作用域如何)允许从定义它的命名空间之外调用子程序

# Perl 6
module Foo {
    our sub bar() { "baz" }  # make sub visible from the outside
}
say Foo::bar();
# baz

如果没有 our,这将失败。在 Perl 5 中,任何子程序都可以从定义它的命名空间之外调用

# Perl 5
package Foo {
    sub bar { "baz" }     # always visible from the outside
}
say Foo::bar();
# baz

在 Perl 5 中,旨在成为“私有”的子程序名称(即,仅从该作用域内而不是从外部调用)通常以下划线开头。但这不会阻止从外部调用它们。在 Perl 6 中,不打算从外部调用的子程序根本不可见。

Perl 6 中子程序定义中的 our 不仅指示可以从外部调用子程序,还指示如果它是正在加载的模块的一部分,则它将被导出。在未来关于模块创建和模块加载的文章中,我将深入探讨导出子程序。

调用子程序

如果在未启用子程序签名的情况下在 Perl 5 中调用子程序,它将调用子程序(在运行时确定)并将参数传递到子程序内部的 @_ 中。子程序内部参数会发生什么完全取决于子程序(请参阅Perl 6 中子程序签名如何工作)。

在 Perl 6 中调用子程序时,它会执行额外的检查,以查看传递给子程序的参数是否与子程序在调用子程序的代码之前期望的参数匹配。 Perl 6 尝试尽早执行此操作——如果它确定对子程序的调用永远不会成功,它会在编译时告诉你

# Perl 6
sub foo() { "bar" }    # subroutine not taking any parameters
say foo(42);           # try calling it with one argument
# ===SORRY!=== Error while compiling ...
# Calling foo(Int) will never work with declared signature ()

请注意,错误消息提到了作为参数传递的值的类型 (Int)。 在这种情况下,调用子程序将失败,因为子程序不接受传递给它的任何参数 (declared signature ())。

其他签名功能

除了在签名中指定位置和命名参数外,还可以指定这些参数应为何种类型。 如果参数类型与参数类型不智能匹配,它将被拒绝。 在此示例中,子程序需要一个 Str 参数

# Perl 6
sub foo(Str $who) { "Hello $who" }  # subroutine taking a Str parameter
say foo(42);                        # try calling it with an integer
# ===SORRY!=== Error while compiling ...
# Calling foo(Int) will never work with declared signature (Str $who)

它同时检查所需参数的数量类型。不幸的是,并非总是可以在编译时可靠地看到这一点。但是当将参数绑定到参数时,仍然会进行运行时检查

# Perl 6
sub foo(Str $who) { "Hello $who" }  # subroutine taking a Str parameter
my $answer = 42;
say foo($answer);                   # try calling it with a variable
# Type check failed in binding to parameter '$who'; expected Str but got Int (42)
#   in sub foo at ...

但是,如果 Perl 6 知道传递给子程序的变量的类型,则可以在编译时确定该调用永远不会成功

# Perl 6
sub foo(Str $who) { "Hello $who" }  # subroutine taking a Str parameter
my Int $answer = 42;
say foo($answer);                   # try calling it with an Int variable
# ===SORRY!=== Error while compiling ...
# Calling foo(Int) will never work with declared signature (Str $who)

应该清楚的是,在你的变量和参数中使用类型可以使 Perl 6 帮助你更快地发现问题!

渐进式类型

以上通常称为渐进式类型。 Perl 6 始终在运行时执行类型检查 (动态类型)。 但是,如果它可以在编译时确定某些东西不起作用,它会告诉你。 这通常称为静态类型

如果你来自 Perl 5 并且有使用 Moose(特别是 MooseX::Types)的经验,你可能会担心向代码添加类型信息对性能的影响。 在 Perl 6 中,这不是一个问题,因为每次分配给变量或绑定参数时,类型检查始终在 Perl 6 中进行。 这是因为如果你未指定类型,Perl 6 将隐式地假定 Any 类型,它与 Perl 6 中的(几乎)所有内容智能匹配。 所以,如果你正在写

# Perl 6
my $foo = 42

实际上你已经写了

# Perl 6
my Any $foo = 42;

子程序的参数也是如此

# Perl 6
sub foo($bar) { ... }

实际上是

# Perl 6
sub foo(Any $bar) { ... }

添加类型信息不仅有助于发现程序中的错误,还可以让优化器更好地了解它可以优化什么以及如何最好地优化它。

已定义或未定义

如果在 Perl 5 中指定一个变量但不分配它,它将包含未定义的值 (undef)

# Perl 5
my $foo;
say defined($foo) ? "defined" : "NOT defined";
# NOT defined

这在 Perl 6 中没有太大区别

# Perl 6
my $foo;
say defined($foo) ?? "defined" !! "NOT defined";
# NOT defined

所以,你可以指定哪些类型的值在变量中和作为参数是可接受的。 但是,如果你不分配这样的变量会发生什么?

# Perl 6
my Int $foo;
say defined($foo) ?? "defined" !! "NOT defined";
# NOT defined

此类变量中的值仍然未定义,就像在 Perl 5 中一样。但是,如果你只想显示此类变量的内容,则不是 undef,就像在 Perl 5 中一样

# Perl 6
my Int $foo;
say $foo;
# (Int)

你看到的是 Perl 6 中类型对象的表示。 与 Perl 5 不同,Perl 6 有多种类型的 undef。 每个已定义或你自己定义的类都是类型对象。

# Perl 6
class Foo { }
say defined(Foo) ?? "defined" !! "NOT defined";
# NOT defined

但是,如果你实例化一个类型对象,通常使用 .new,它会像预期的那样成为一个已定义的对象

# Perl 6
class Foo { }
say defined(Foo.new) ?? "defined" !! "NOT defined";
# defined

类型表情

如果在子程序中指定参数的约束,你还可以指示是否需要该类型的已定义值

# Perl 6
sub foo(Int:D $bar) { ... }

:DInt 结合表示你需要 Int 类型的 Defined 值。 因为 :D 也是一个大笑的表情符号,所以类型上的这种装饰被称为“类型表情”。 那么,如果你将未定义的值传递给这样的子程序会发生什么?

# Perl 6
sub foo(Int:D $bar) { ... }   # only accept instances of Int
foo(Int);                     # call with a type object
# Parameter '$bar' of routine 'foo' must be an object instance of
# type 'Int', not a type object of type 'Int'.  Did you forget a '.new'?

细心的读者可能会意识到这应该会产生一个编译时错误。 但唉,它还没有(还)。 尽管众所周知 Perl 6 中的错误消息非常棒,但仍有很多工作要做,以使它们更好(并且在这种情况下更及时)。

你还可以使用 :D 类型表情来定义变量,以确保你为该变量提供初始化

# Perl 6
my Int:D $foo;                # missing initialization
# ===SORRY!=== Error while compiling ...
# Variable definition of type Int:D requires an initializer

其他类型表情有 :U(用于定义)和 :_(用于不在乎,这是默认值)

# Perl 6
sub foo(Int:U $bar) { ... }   # only accept Int type object
foo(42);                      # call with an instance of Int
# Parameter '$bar' of routine 'foo' must be a type object of type 'Int',
# not an object instance of type 'Int'.  Did you forget a 'multi'?

嗯……这个似乎被遗忘的 multi 是什么? 好吧,那是本系列下一篇文章的内容!

总结

默认情况下,Perl 6 中的子程序仅在定义它们的词法范围内可见。 即使添加 our 前缀也不会使它们在该词法范围之外可见,但它确实允许使用其完整包名称(包 bar 中的子程序 Foo::bar())从范围外部调用子程序。

Perl 6 允许你使用渐进式类型来确保子程序的参数或变量赋值的有效性。 这会产生任何额外的运行时成本。 将类型添加到你的代码甚至允许编译器在编译时捕获错误,并且它允许优化器在运行时做出更好的优化决策。

接下来阅读什么
标签
Elizabeth Mattijsen
Elizabeth Mattijsen自1978年以来一直从事编程工作,使用各种(现在大多已失效的)编程语言,然后才开始使用Perl 4进行编程。1994年,她在荷兰创立了第一家商业网站开发公司,使用Perl 5作为主要编程语言。从2003年开始,她参与了一家在线酒店预订服务的快速增长。

评论已关闭。

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.