第五章. 创建块

Building Blocks

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第五章. 创建块

存储 Blocks

你可以把 Block 存储在一个变量中而不立即执行它。now 是一个内置项, 它能够给你一个 Instant。使用 := 进行绑定让右侧和左侧一样。这意味着 $block 和 Block 相同:

my $block := { now };

你不能对 $block 赋值, 因为没有涉及到容器。

你本没必要绑定到 Block 的。赋值也是可以的, 并且你可以在后面更改值:

my $block = { now };
$block = 'Hamadryas';

这不是那么有趣, 因为你可以在你能使用 now 的任何地方使用它。但是计算一个 1 分钟之后的时间的 Block 怎么样?给 now 加上 60 秒:

my $minute-later := { now + 60 };

当你执行 Block 的时候, 它的结果是最后一个被求值的表达式的值。使用 () 操作符执行 Block:

put $minute-later();  # some Instant
sleep 2;
put $minute-later();  # some Instant 62 seconds later

因为 $block 是一个对象, 你可以像方法那样调用 ():

put $minute-later.();  # some Instant
sleep 2;
put $minute-later.();  # some Instant 62 seconds later

你可以使用 callable 变量来代替标量变量;下面这些使用 & 符号:

my &hour-later := { now + 3_600 };

put &hour-later();  # some Instant
sleep 2;
put &hour-later();  # some Instant an hour later

使用 &block 形式你可以在调用的时候不带 & 符号, 甚至不带圆括号:

my &hour-ago := { now - 3_600 };

put &hour-ago();   # some Instant
sleep 2;
put hour-later();  # some Instant two seconds later

put hour-ago;      # some Instant immediately

任何一种方式, Block 都不是一个子例程(即将到来的),所以你不能使用 return(稍后将详细介绍)。它不知道怎么将结果传递给调用它的代码。下面这段代码即使不起作用,它也会编译:

my $block := -> { return now };

你会得到一个运行时错误,你会在第十一章了解更多关于它的:

在任何例程外面尝试返回

带参数的 Blocks

签名定义了 Block 的参数。这包含了你给予 Block 在参数上的个数(参数数量),类型和约束。

如果 Block 没有签名那么它期望零个参数。然而,如果你在 Block 中使用 $_,那么它创建了一个带有单个可选参数的签名:

my $one-arg := { put "The argument was $_" };

$one-arg();             # The argument was (with warning!)
$one-arg(5);            # The argument was 5
$one-arg('Hamadryas');  # The argument was Hamadryas

如果你更改那个 $_,那么你就更改了原始值(如果它是可变的)因为隐式的签名让那个参数可写:

my $one-arg := {
    put "The argument is $_";
    $_ = 5;
    };

my $var = 'Hamadryas';
say "\$var starts as $var";
$one-arg($var);
say "\$var is now $var";    

输出显示了 Block 更改了变量的值:

$var starts as Hamadryas
The argument is Hamadryas
$var is now 5

如果你在 Block 中使用 @_,那么你可以传递零个或多个参数:

my $many-args := {
    put "The argument are @_[]";
    }

$many-args( 'Hamadryas', 'perlicus' );

其中 @_ 是一个 Array,但是你必须等到下一章才能看到那些能做些什么。

练习 5.3

创建一个移除尾部空白并小写化它的参数的 Block。原始值可能更改。当你正态化数据的时候你可能想使用这些东西。

隐式参数

Blocks 变得更漂亮。你可以在 Block 里面使用占位符变量(或隐式参数)来指定你需要多少个参数:

my $adding-block := { $^a + $^b }

^ 表示一个占位符变量,它告诉编译器为 Block 构建一个隐式签名。你的 Blocks 拥有和占位符值同样多的参数个数并且你必须为每一个参数提供一个实参:

my $adding-block := { $^a + $^b };

$adding-block();           # Nope - too few parameters
$adding-block( 1 );        # Nope - too few still
$adding-block( 1, 37 );    # Just right!
$adding-block( 1, 2, 3 );  # Nope - too many parameters

参数是根据它们名字的字典顺序而不是你使用他们的顺序赋值给占位符变量的。下面的这些 Blocks 把两个数相除;不同之处在于它们使用占位符变量的顺序:

my $forward-division  := { $^a / $^b };
my $backward-division := { $^b / $^a };

你可以用同样的参数以同样的顺序来调用它们。即使你使用相同的占位符名并且传递相同的参数,但是你得到不同的答案:

put $forward-division( 2, 3 );   # 0.66667
put $backward-division( 2 ,3 );  # 1.5

你可以重用同一个占位符变量而不需要创建额外的参数。下面这个仍然是一个参数并且这个参数和自身相乘:

my $square := { $^a * $^a }

调用 .signature 会给你那个 Block 的 Signature 对象。使用 say 输出它会给你一个 .gist 表示:

my $square := { $^a * $^a }
say $square.signature;   # ($a)

练习 5.4

创建一个使用三个占位符变量的 Block 并计算三个数中的最大的数。 max 例程能帮到你。使用不同的参数运行这个 Block。

显式签名

尖尖的箭头是签名的开始,在签名里面你可以指定你的参数。-> 和 { 之间什么都没有的话,那么你的签名拥有零个参数:

my $block := -> { put "You called this block"; };

当你调用这个 Block 的时候你不必指定参数:

put $block();     # No argument, so it works
put $block( 2 );  # Error - too many parameters

在 -> 和 { 之间定义形参:

my $block := -> $a { put "You called this block with $a"; };

签名中形参的顺序决定了实参填充的顺序。如果 $b 是第一个形参,那么它就获取第一个实参。它们的词典顺序不影响:

my $block := -> $b, $a { $a / $b };
put $block( 2, 3);  # 1.5
put $block( 3, 2);  # 0.666667

这些参数是位置参数。还有另外一种形式的参数,其中你能指定哪个行参得到哪个实参。这些是命名参数:

my $block := -> :$b, :$a { $a / $b };
put $block( b => 3, a => 2 );  # 0.666667
put $block( a => 3, b => 2 );  # 1.5

你会在第十一章看到更多关于签名的东西,但是这些已经足够你入门了。

类型约束

形参变量可以约束它们允许的类型。下面这个 Block 数值上进行俩个值相除但是它不强制你给它传数字:

my $block := -> $b, $a { $a / $b };
$block( 1, 2 );
$block( 'Hamadryas', 'perlicus' );

第二个调用失败了:

Cannot convert string to number: base-10 number must begin with valid digits …

它在 Block 里面失败了。它根本就没到达代码里面。如果你正在做数值操作你应该只允许数字:

my $block := -> Numeric $b, Numeric $a { $a / $b };

put $block( 1, 2 );
put $block( 'Hamadryas', 'perlicus' );

第一个调用有效但是第二个调用尝试使用 Str 但是失败了:

2 Type check failed in binding to parameter ‘$b’; expected Numeric but got Str (“Hamadryas”)

如果 Numeric 类型对你来说太宽了,选择另一种类型:

my $block := -> Int $b, Int $a { $a / $b };

这仍然有个问题,尽管。那个 Int 约束允许任何智能匹配为 Int 的东西。 Int 类型对象满足这个约束:

$block( Int, 3 ); # 调用仍旧有效

这让它通过了形参守卫然后在除法里面失败了。在类型的后面添加一个 :D 来约束参数为一个有定义的值。类型对象总是未定义的:

my $block := -> Int:D $b, Int:D $a { $a / $b };

你会在第十一章看到更多关于签名的信息。

简单子例程

子例程是带有额外功能的代码块。代替尖尖的箭头,这里你使用 sub:

my $subroutine := sub { put "Called subroutine!" };

你以同样的方式执行它:

$subroutine();

子例程可以返回值(Block 不能)。调用 Sub 会计算一些值并在所调用的作用域里让你可见。

之前的 Blocks 处理了输出。从子例程里处理输出通常不是一种好形式,因为它做了两份工作:计算值然后输出。它不够灵活因为它决定了怎么处理值。返回值让你稍后决定:

my $subroutine := sub { return "Called subroutine!" };
put $subroutine();

你应该保存结果而不是输出结果:

my $result = $subroutine();

return 退出最里层的例程(Sub 的超类)。如果一个 Block 在某种 Routine 之内, 你可以在那个 Block 里面使用一个 return,然后你立即执行该 Block。这会立即结束该子例程:

my $subroutine := sub {
    -> { # not a sub!
       return "Called subroutine!"
    }.(); # 立即执行

    put 'This is unreachable and will never run';
    };

put $subroutine();   # Called subroutine!

你很可能在使用 Block 的东西上使用它,例如 if 结构。所有的这些 Blocks 都能使用 return,因为它们在 Routine 之内,它知道如何处理它:

my $subroutine := sub {
    if now.Int %% 2 { return 'Even' }
    else            { return 'Odd'  }
    };

put $subroutine();

do if 只需要一个 return:

my $subroutine := sub {
    return do if now.Int %% 2 { 'Even' }
              else            { 'Odd'  }
    };

put $subroutine();    

命名子例程

子例程可以拥有名字。在 sub 后面指定名字。然后你可以通过子例程的名字执行子例程,和通过它的变量执行一样。它们做同样的事情因为它们实际上是同样的东西:

my $subroutine := sub show-me { return "Called subroutine!" };
put $subroutine.(); # Called subroutine!
put show-me();      # Called subroutine!

通常你会把变量也一块儿跳过:

sub show-me { return "Called subroutine!" };
put show-me();      # Called subroutine!

要定义子例程的签名,把签名放在子例程名字后面的圆括号中(这和 Block 稍微有点不同):

sub divide ( Int:D $a, Int:D $b ) { $a / $b }
put divide( 5, 7 );  # 0.714286

如果它不会使解析器产生歧义,你可以省略掉圆括号。这是同样的东西:

put divide 5, 7;     # 714286

这个子例程定义是一个表达式,就像 Block 那样。如果你在闭合花括号之后还有除了末尾空格之外的其它东西,则需要分号:

sub divide ( Int:D $a, Int:D $b ) { $a / $b }; put divide( 5, 7 );

子例程默认是词法作用域的。如果你在 Block 里面定义一个子例程,那么它只存在于 Block 里面。外部作用域不知道 divide 的存在,所以这是错误的:

{
    sub divide ( Int:D $a, Int:D $b ) { $a / $b }
    put divide( 5, 7 );
}

put divide( 3, 2 );  # Error!

这和词法变量名拥有同样的优点:你不必知道所有其它的子例程来定义你自己的。这也意味着如果你有一个想要临时替代的子例程,你可以在你需要的作用域里创建你自己的版本:

sub divide ( Int:D $a, Int:D $b ) { $a / $b }
put divide 1, 137;

{ # a scope for the fixed version of divide
sub divide ( Numeric $b, Numeric $a ) {
    put "Calling my private divide!";
    $a / $b
    }

put divide 1.1, 137.003;    
}

Whatever Code

这一章的目的是为了接触这个遍及语言的有趣的特性。在下面几章你会需要这个特性。

Whatever, 也就是 *,是某个东西的替身,你会在之后填充它。填充到它里面的东西决定了它应该是什么。这儿来看看它在表达式中长什么样,让某个东西加上 2:

my $sum = * + 2;

你知道这儿的 * 不是用于乘法,因为乘法需要两个操作数。所以发生了什么?编译器认出了 * 并创建了一个 WhateverCode(也叫做形实转换程序)。它是一个没有定义自己的作用域的代码段,但是不被立即执行。它最像带有一个参数的 Block:

my $sum := { $^a + 2 }

调用带有一个参数的 WhateverCode 来获取最后的值:

$sum = * + 2;
put $sum( 135 );   # 137

获取你想要两个参数。你可以使用两个 * 并且你的 WhateverCode 会接受两个参数:

my $sum = * + *;
put $sum( 135, 2 );   # 137

Whatever * 出现在很多其它有趣的结构中;这是你为什么这么早阅读本书中关于子例程的东西的原因。现在就有两个有趣的用途。

Subsets

WhateverCode 允许你把代码插入到语句中。你可以用它们来创建更有趣的带有 subset 和 where 的类型。首先定义一个不带约束的新类型。你告诉 subset 你想以哪个已存在的类型开始。下面创建一个和 Int 同样的类型:

subset PositiveInt of Int;
my PositiveInt $x = -5;
put $x;

这在运行时检查赋值。你放到 $x 中的类型必须是一个 PositiveInt,但是(目前为止)它和 Int 一样。 -5 是一个 Int 数, 所以这正常工作。

现在通过指定一个带有代码块儿的 where 子句来约束 Int 的合法值:

subset PositiveInt of Int where { $^a > 0 };
my PositiveInt $x = -5;
put $x;

当你给 $x 赋值时你会触发运行时类型检查。变量 $x 知道它必须是一个 PositiveInt。它接受一个可能适合 PositiveInt 的值并把它传入 where 子句中的 Block。如果那段代码计算为 True,那么变接受那个值。如果计算结果为 False,你会得到一个错误:

Type check failed in assignment to $x; expected PositiveInt but got Int (-5)

Whatever 允许你省略一些打字。* 会为你做大部分工作。它代表你想要测试的东西。这些不是完整的类型但是表现得像它们本来的那样:

subset PositiveInt of Int where * > 0;
my PositiveInt $x = -5;
put $x;

一旦你有了 subset,你就能在签名中使用它了。如果参数不是正整数你得到一个运行时错误:

subset PositiveInt of Int where * > 0;

sub add-numbers ( PositiveInt $n, PositiveInt $m ) {
    $n + $m
    }

put add-numbers 5, 11;    # 16
put add-numbers -5, 11;   # Error

你不需要定义一个显式的 subset,尽管。你可以在签名中使用 where。当你只需要约束一次时这很有用:

sub add-numbers ( $n where * > 0, $m where * > 0 ) {
    $n + $m
    }

随着你继续你会看到更多的 subsets;不用很多代码来限制值,它们很方便。你还没有读到关于模块的东西,但是 Subset::Common 有几个例子,你可能会决定不错。

练习 5.5

使用 subset 创建一个不允许分母为零的 divide 子例程。

总结

这一章提供了子例程简明介绍和像代码那样的东西。有简单的 Block 以组织代码并定义作用域,还有更复杂的子例程,知道怎么样回传值给它的调用者。它们中的每一个都有复杂的方式来处理参数。这一章让你了解足够的细节,所以你可以在下面几章中使用它们。你会在第十一章看到更强大的子例程特性。

comments powered by Disqus