第十一章. 子例程

Subroutines

声明

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

第十一章. 子例程

Now it’s time for more sophisticated subroutines. You were introduced to them in Chapter 5 but you only saw enough to support the upcoming chapters. Now that you’ve seen Arrays and Hashes, there’s much more you can do with subroutine signatures.

现在是更复杂的子例程的时候了。你在第五章见过它们了,但你只看到足以支持即将到来的章节。现在你已经看过数组哈希,你可以用子例程签名做更多的事情。

A Basic Subroutine

When you run a subroutine you get some sort of result: the last evaluated expression. That’s the return value. This sets basic routines apart from the simpler Blocks you saw in Chapter 5. A Routine knows how to send a value back to the code that called it. This subroutine returns a different Str if the argument is odd or even:

当您运行子例程时,您会得到某种结果:最后被计算的表达式。这是返回值。这将基础子例程和你在第五章中看到的更简单的 Block 区分开。例程知道如何将值发送回调用它的代码。如果参数为奇数或偶数,则此子例程返回不同的字符串

sub odd-or-even {
    if ( @_[0] %% 2 ) { 'Even' }
    else              { 'Odd'  }
    }

odd-or-even( 2 );    # Even
odd-or-even( 137 );  # Odd

Without a signature the arguments show up in @_. Each subroutine has its own version of that variable so it doesn’t conflict with any other subroutine’s @_. This code calls one subroutine that calls another. top-callshows its @_ before and after show-args:

如果没有签名,则参数会出现在 @_中。每个子例程都有自己的变量版本,因此它不会与任何其它子例程的@_ 冲突。此代码调用一个调用另一个子例程的子例程。 top-callshows 显示 show-args 之前和之后的 @_

top-call( <Hamadryas perlicus> );

sub show-args { say @_ }
sub top-call {
    put "Top: @_[]";
    show-args( <a b c> );
    put "Top: @_[]";
    }

Even though both use @_ they are separate. The @_ in top-call isn’t disturbed by show-args:

尽管两者都使用 @_,但它们是分开的。 top-call 中的 @_ 不受 show-args 的干扰:

Top: Hamadryas perlicus
[a b c]
Top: Hamadryas perlicus

The subroutine definition is lexically scoped. If you need it for only part of the code you can hide it in a Block. Outside the Block that subroutine is not visible:

子例程定义是词法范围的。如果只需要部分代码,则可以将其隐藏在 Block 中。在 Block 之外,子例程不可见:

{
put odd-or-even( 137 );
sub odd-or-even { ... } # only defined in this block
}

put odd-or-even( 37 );  # undeclared routine!

额外的参数

What does odd-or-even accept, though? The parameter is an Array but you only use the first element. These calls still work without warnings or errors:

但是 odd-or-even 接受了什么?参数是一个数组,但您只使用第一个元素。这些调用仍然有效,没有警告或错误:

put odd-or-even( 2, 47        );  # Even
put odd-or-even( 137, 'Hello' );  # Odd

This isn’t necessarily wrong. It depends on what you are trying to do. Maybe you specifically want as many arguments as the caller decides to send:

这不一定是错的。这取决于你想要做什么。也许你特别想要调用者决定发送尽可能多的参数:

sub plus-minus {
    [-]
    @_
        .rotor(2, :partial)
        .map: { $^a[0] + ($^a[1] // 0) }
    }

put plus-minus( 9,1,2,3 );

With the signatures you’ll see later in the chapter you’ll be able to control this to get the situation that you want.

使用您将在本章后面看到的签名,您将能够控制它以获得您想要的情况。

显式返回

You can explicitly return from anywhere in a subroutine with return. This distinguishes a subroutine from the Blocks you used in Chapter 5. This version is the same thing but with an explicit return:

您可以使用 return 从子例程中的任何位置显式返回。这将子例程与您在第五章中使用的 Block 区分开来。这个版本是相同的,但具有明确的返回:

sub odd-or-even ( $n ) {
    if ( $n %% 2 ) { return 'Even' }
    else           { return 'Odd'  }
    }

Call this with extra arguments and you get an error:

使用额外的参数调用此方法会出现错误:

put odd-or-even( 2, 47 );  # Error

The message tells you the argument list does not match the signature:

该消息告诉您参数列表与签名不匹配:

Calling odd-or-even(Int, Int) will never work with declared signature ($n)

You could write this differently. do converts the entire if structure into something that evaluates to its last evaluated expression. Return the value of the do instead of repeating the return:

你可以用不同的方式写出来。do 将整个 if 结构转换为计算其最后计算的表达式的内容。返回 do 的值而不是重复 return

sub odd-or-even ( $n ) {
    return do {
        if ( $n %% 2 ) { 'Even' }
        else           { 'Odd'  }
        }
    }

The conditional operator is the same thing expressed differently:

条件运算符是同样的东西,它以不同的方式表达:

sub odd-or-even ( $n ) {
    return $n %% 2 ?? 'Even' !! 'Odd'
    }

Another way to do the same thing is to have a default return value but return sooner for other situations:

另一种做同样事情的方法是使用默认返回值,但在其他情况下更快返回:

sub odd-or-even ( $n ) {
    return 'Even' if $n %% 2;
    'Odd';
    }

Or back to where you started with an implicit return:

或者回到你开始的隐式返回:

sub odd-or-even ( $n ) { $n %% 2 ?? 'Even' !! 'Odd' }

These techniques are more appealing in more complex situations that I’m not going to show here. No matter which of these serves your situation, they all do the same thing: they send a value back to the code that called it.

这些技术在更复杂的情况下更具吸引力,我不会在这里展示。无论哪种情况适合您的情况,它们都会做同样的事情:它们将值发送回调用它的代码。

EXERCISE 11.1Create a subroutine that returns the least common multiple of two integers. Use that in a program that takes two integers from the command line. The particulars of this exercise are very simple but it’s the structure of the subroutine definitions that matter.

练习11.1创建一个返回两个整数的最小公倍数的子程序。在从命令行获取两个整数的程序中使用它。这个练习的细节非常简单,但重要的是子程序定义的结构。

递归

Subroutines can call themselves; this is called recursion. The classic example is Fibonacci numbers, where the next number in the series is the sum of the preceding two given that the first two numbers are 0 and 1:

子程序可以调用自己;这称为递归。典型的例子是 Fibonacci 数,其中系列中的下一个数字是前两个的总和,前两个数字是 0 和 1:

sub fibonacci ( $n ) {
    return 0 if $n == 0;  # special case of n = 0
    return 1 if $n == 1;
    return fibonacci( $n - 1 ) + fibonacci( $n - 2 );
    }

say fibonacci( 10 );  # 55

When you call this subroutine with the value of 10 it calls itself twice to get the values for 9 and 8. When it calls itself for 9, it creates two more calls for 8 and 7. It keeps creating more and more calls to itself until the arguments are 0 or 1. It can then return a value one level up, working its way back to where it started.

当您使用值 10 调用此子例程时,它会调用自身两次以获取 98 的值。当它自己调用 9 时,它会为 87 创建两个以上的调用。它会不断创建对自身的调用,直到参数为 01.然后它可以返回一个级别的值,然后返回到它开始的位置。

A Raku subroutine knows what it is inside its own Block. The variable &?ROUTINE is the same subroutine object. You don’t have to know the current subroutine’s name. This is the same thing:

Raku 子程序知道它自己的 Block 内部是什么。变量&?ROUTINE是相同的子程序对象。您不必知道当前子例程的名称。这是同样的东西:

sub fibonacci ( $n ) {
    return 0 if $n == 0;
    return 1 if $n == 1;
    return &?ROUTINE( $n - 1 ) + &?ROUTINE( $n - 2 );
    }

This is only slightly better. You’ll read more about this later when you encounter multi subroutines.

这稍微好一点。当您遇到 multi 子程序时,您将在以后阅读更多相关信息。

EXERCISE 11.2Another favorite example of recursion is the factorial function. Start with a positive whole number and multiply it by all the strictly positive numbers that come before it. The factorial of 6 would be 65432*1. Implement this as a recursive function. Once you’ve done that, implement it in the amazingly simple Raku fashion. How big a number can you get your program to produce?

练习11.2递归的另一个例子是阶乘函数。从正整数开始,然后乘以前面的所有严格正数。阶乘为6将为65432 * 1。将其实现为递归函数。完成后,以非常简单的Raku方式实现它。你的程序可以产生多大的数字?

迭代而不是递归

You can turn many recursive solutions into iterative ones. Instead of repeatedly calling subroutines with all the overhead they entail (each call sets up a new scope, defines new variables, and so on), rearrange things so you don’t need a subroutine.

您可以将许多递归解决方案转换为迭代解决方案。而不是重复调用子程序,它们需要所有开销(每个调用设置一个新的作用域,定义新的变量,等等),重新排列事物,这样你就不需要子程序了。

The factorial case is easy. The reduction operator does that for you:

阶乘这个情况很容易。规约运算符为您执行此操作:

my $factorial = [*] 1 .. $n;
注意

The operators are actually methods, so you don’t actually avoid calling something.

运算符实际上是方法,所以你实际上避免不了调用某些东西。

The Fibonacci case is easy too when you use a Seq:

使用 Seq 时,Fibonacci 案例也很简单:

my $fibonacci := 1, 1, * + * ... *;
put "Fib(5) is ", $fibonacci[5];

You can make a queue of things to work on. With a queue you can add items anywhere you like. Instead of processing the next thingy immediately you can put it at the end of the queue. When it’s time to process the next thingy you can take it from the beginning, end, or middle. You can add as many elements as you want:

您可以创建一个可以处理的事物队列。使用队列,您可以在任何地方添加项目。不要立即处理下一个东西,而是将它放在队列的末尾。当处理下一件事时,你可以从开始,结束或中间开始。您可以根据需要添加任意数量的元素:

my @queue = ( ... );
while @queue {
    my $thingy = @queue.shift; # or .pop
    ... # generate more items to process
    @queue.append: @additional-items; # or .push or .unshift
    }

在库中存储子程序

Start with a simple subroutine to choose a random integer between two integers (including the endpoints). Use .rand and coerce the result with .Int, then shift the result into the right range:

从一个简单的子程序开始,选择两个整数(包括端点)之间的随机整数。使用 .rand 并使用 .Int 强制结果,然后将结果移动到正确的范围:

sub random-between ( $i, $j ) {
    ( $j - $i ).rand.Int + $i;
    }

say random-between( -10, -3 );

You might need to convince yourself that works. Your program gets its job done, so you don’t think about it again. Then you write a different program doing something similar and you want to use that subroutine again. You do what many people don’t want to admit to: you cut and paste the subroutine into a different program. Again, it works. Or does it?

你可能需要说服自己有所作为。你的程序完成了它的工作,所以你不要再考虑它了。然后你编写一个不同的程序做类似的事情,你想再次使用该子程序。你做了许多人不愿意承认的事情:你将子程序剪切并粘贴到另一个程序中。再次,它的工作原理。或者是吗?

Did you really get a number between $i and $j inclusively?

你真的得到了$ i和$ j之间的数字吗?

EXERCISE 11.3What’s the maximum number that random-between produces for any $i and $j? Write a program that figures it out by running random-between repeatedly to see the actual range of results.

练习11.3任意$ i和$ j之间随机产生的最大数量是多少?编写一个程序,通过反复运行随机查看结果的实际范围来计算出来。

Once you’ve done that exercise you know that random-between didn’t ever select the second endpoint as one of the random values. If you had copied it into other programs it would have been wrong in several places. There’s a way to fix that.

一旦你完成了这个练习,你就知道随机中断之间并没有选择第二个端点作为随机值之一。如果你把它复制到其他程序中,它会在几个地方出错。有办法解决这个问题。

To use the same subroutine in several programs you can define it once in a library. That’s a separate file that you can import into your program.

要在多个程序中使用相同的子程序,可以在库中定义一次。这是一个单独的文件,您可以导入到您的程序中。

Move random-between to a new file that has the .pm or .pm6 extension:

随机移动到具有.pm或.pm6扩展名的新文件:

# MyRandLibrary.pm6
sub random-between ( $i, $j ) {
    ( $j - $i ).rand.Int + $i;
    }

In your original program import your library with use. Set lib as you saw in Chapter 10:

在您的原始程序中导入您的库使用。按照第10章中的说明设置lib:

# random-between.p6
use lib <.>
use MyRandLibrary;
say random-between( -10, -3 );

Your program finds your library but now you get a different error:

您的程序找到了您的库,但现在您收到了另一个错误:

% raku random-between.p6
===SORRY!=== Error while compiling ...
Undeclared routine:
    random-between used at line ...

导出子程序

Subroutines are lexically scoped by default, so they can’t be seen outside their files. If you want another file to use them you need to export those subroutines. The is export trait does that and comes right after the signature:

默认情况下,子例程在词法上是作用域的,因此不能在文件外看到它们。如果您想要使用其他文件,则需要导出这些子例程。出口特征是这样做的,并在签名后立即出现:

# MyRandLibrary.pm6
sub random-between ( $i, $j ) is export {
    ( $j - $i ).rand.Int + $i;
    }

Your program now finds the library, imports the subroutine, and produces a number between your endpoints:

您的程序现在找到库,导入子例程,并在端点之间生成一个数字:

% raku random-between.p6
11

EXERCISE 11.4Create the library that exports the random-between subroutine and use it in a program to get a random number between the two command-line arguments. What happens when the first argument is greater than the second? What happens if one of the arguments is not a number?

练习11.4创建导出random-between子例程的库,并在程序中使用它来获取两个命令行参数之间的随机数。当第一个参数大于第二个参数时会发生什么?如果其中一个参数不是数字,会发生什么?

位置参数

There are two types of parameters. The first are the positional parameters that you’ve seen already in Chapter 5. These parameters handle the arguments by their order in the argument list. We’ll look at them in a bit more detail here. You’ll see the other sort, named parameters, later in this chapter.

有两种类型的参数。第一个是您在第5章中已经看到的位置参数。这些参数按参数列表中的顺序处理参数。我们将在这里详细介绍它们。您将在本章后面看到另一种命名参数。

With no explicit signature the arguments show up in the array @_. Each subroutine gets its own @_ so it doesn’t conflict with that for any other subroutine. So, if you write this:

没有明确的签名,参数显示在数组@中。每个子程序都有自己的@,因此它不会与任何其他子程序冲突。所以,如果你这样写:

sub show-the-arguments {
    put "The arguments are: ", @_.gist;
    }

show-the-arguments( 1, 3, 7 );

You get:

你得到:

The arguments are: [1 3 7]

Using @_ inside the subroutine automatically adds the implicit signature. But it’s not as simple as an explicit @_parameter by itself. This signature expects a single Positional argument:

在子例程中使用@_会自动添加隐式签名。但它本身并不像显式的@_parameter那么简单。此签名需要一个Positional参数:

sub show-the-arguments ( @_ ) { # Single Positional argument
    put "The arguments are: ", @_.gist;
    }

Calling it with multiple arguments is a compile-time error:

使用多个参数调用它是编译时错误:

show-the-arguments( 1, 3, 7 );   # Won't compile

The ( @_ ) signature wants a single argument that’s some sort of Positional (not necessarily an Array):

(@_)签名需要一个参数,它是某种Positional(不一定是数组):

show-the-arguments( [ 1, 3, 7 ] );   # Single argument

Slurpy Parameters 吞噬参数

A slurpy parameter gets all of the remaining arguments into a single Array. Prefix the array parameter with a *. This is the same as the version with no explicit signature:

一个slurpy参数将所有剩余的参数都放入一个Array中。使用*前缀数组参数。这与没有显式签名的版本相同:

sub show-the-arguments ( *@_ ) {  # slurpy
    put "The arguments are: ", @_.gist;
    }

show-the-arguments( 1, 3, 7 );

The output shows the three numbers:

输出显示三个数字:

The arguments are: [1 3 7]

There’s not much special about @_. You can use your own variable name instead:

@_没什么特别之处。您可以使用自己的变量名称:

sub show-the-arguments ( *@args ) {  # slurpy
    put "The arguments are: ", @args.gist;
    }

Try it with something slightly different now. Include a List as one of the arguments:

现在尝试使用略有不同的东西。包含List作为参数之一:

sub show-the-arguments ( *@args ) {  # slurpy
    put "The arguments are: ", @args.gist;
    }

show-the-arguments( 1, 3, ( 7, 6, 5 ) );

Did you expect this output? It’s a flat List with no structure:

你有没有期待这个输出?这是一个没有结构的平面列表:

The arguments are: [1 3 7 6 5]

This isn’t a problem with formatting the data; the slurpy parameter flattens the data. Try it again with another level:

这不是格式化数据的问题; slurpy参数使数据变平。再试一次另一个级别:

show-the-arguments( 1, 3, ( 7, (6, 5) ) );

You get the same output with no structure:

你得到相同的输出,没有结构:

The arguments are: [1 3 7 6 5]

The slurpy parameter only flattens objects that you can iterate. If you itemize one of the Lists that item is no longer iterable. Items resist flattening:

slurpy参数仅展平您可以迭代的对象。如果您列出其中一个列表,则该项目不再可迭代。物品抗压扁:

show-the-arguments( 1, 3, ( 7, $(6, 5) ) );

The output is a bit different:

输出有点不同:

The arguments are: [1, 3, 7, (6, 5)]

How about this one?

这个怎么样?

show-the-arguments( [ 1, 3, ( 7, $(6, 5) ) ] );

Instead of a List you have an Array. Remember that an Array already itemizes each of its elements. The ( 7, $(6, 5) ) is itemized because it’s an element of an Array:

而不是List你有一个数组。请记住,数组已经逐项列出了每个元素。 (7,$(6,5))是逐项列出的,因为它是数组的一个元素:

The arguments are: [1, 3, (7, $(6, 5))]

Use a ** in front of the parameter if you don’t want this automatic flattening:

如果您不希望这种自动展平,请在参数前面使用 **

sub show-nonflat-arguments ( **@args ) {  # nonflattening slurpy
    put "The nonflat arguments are: ", @args.gist;
    }

show-nonflat-arguments( [ 1, 3, ( 7, $(6, 5) ) ] );

This output has a double set of square brackets around the data. The single argument is the inner Array and the entire argument list is the outer one:

此输出在数据周围有一组双方括号。单个参数是内部数组,整个参数列表是外部数据:

The nonflat arguments are: [[1 3 (7 (6 5))]]

EXERCISE 11.5Create a subroutine that outputs its argument count and shows each argument on a separate line. Try it with these argument lists:1, 3, 7 1, 3, ( 7, 6, 5 ) 1, 3, ( 7, $(6, 5) ) [ 1, 3, ( 7, $(6, 5) ) ]

练习11.5创建一个子程序,输出其参数计数并在一个单独的行上显示每个参数。尝试使用这些参数列表:1,3,7 1,3,3(7,6,5)1,3,(7,$(6,5))[1,3,(7,$(6, 5))]

Have It Both Ways

What if you want both flattening and nonflattening at the same time? If there’s one argument, you want to flatten that. If there’s more than one argument you want to leave that List alone. Use a + in front of a parameter to use the single argument rule:

如果你想要同时展平和不展平怎么办?如果有一个论点,你想要弄平。如果有多个参数,您希望单独保留该列表。在参数前面使用+来使用单个参数规则:

sub show-plus-arguments ( +@args ) {  # single argument rule
    put "There are {@args.elems} arguments";
    put "The nonflat arguments are: ", @args.gist;
    }

If you pass one argument that argument is flattened into @args. With more than one argument you don’t get flattening:

如果你传递一个参数,将参数展平为@args。如果有多个参数,你就不会变平:

my @a = (1,3,7);

show-plus-arguments( @a );    # flattened
show-plus-arguments( @a, 5 ); # not flattened

The output shows the difference. In your first call to show-plus-arguments it looks like you have single Array argument. By the time it gets inside the subroutine that Array has been flattened into three Intarguments:

输出显示差异。在第一次调用show-plus-arguments时,看起来你有单个Array参数。当它进入子程序时,Array已被展平为三个Intarguments:

There are 3 arguments
The nonflat arguments are: [1 3 7]
There are 2 arguments
The nonflat arguments are: [[1 3 7] 5]

Your second call has the Array along with 5. With more than one argument you don’t get flattening and the argument list has an Array argument and an Int argument.

你的第二个调用有数组和5.有多个参数你没有变平,参数列表有一个Array参数和一个Int参数。

Combining Slurpies

You can have only one slurpy Array parameter, since it will take up the rest of the positional arguments. However, you can have positional parameters before a slurpy parameter:

您可以只有一个slurpy Array参数,因为它将占用其余的位置参数。但是,您可以在slurpy参数之前获得位置参数:

sub show-the-arguments ( $i, $j, *@args ) {  # slurpy
    put "The arguments are i: $i j: $j and @args[]";
    }

show-the-arguments( 1, 3, 7, 5 );

The first two arguments fill in $i and $j and anything left over goes into @args:

前两个参数填写$ i和$ j,剩下的任何内容都会输入@args:

The arguments are i: 1 j: 3 and 7 5

What if you put all but one of the arguments into an Array?

如果将除一个参数之外的所有参数放入数组中该怎么办?

my @a = ( 3, 7, 5 );
show-the-arguments( 1, @a );

Now the output shows that $j has an Array value and @args has nothing:

现在输出显示$ j有一个Array值而@args什么都没有:

The arguments are i: 1 j: 3 7 5 and

EXERCISE 11.6Create a library that provides a head and a tail function that each take a List parameter. Make your headfunction return the first item in the List and your tail function return everything else. Do not change the original List. If you’re used to Lisp you might call these car and cdr:use lib <.>; use HeadsTails; my @a = <1 3 5 7 11 13>; say head( @a ); # 1 say tail( @a ); # [ 3 5 7 11 13 ]

练习11.6创建一个提供head和tail函数的库,每个函数都有一个List参数。让你的headfunction返回List中的第一项,你的tail函数返回其他所有内容。请勿更改原始列表。如果您习惯使用Lisp,可以将这些汽车和cdr称为:使用lib <。>;使用HeadsTails;我的@a = <1 3 5 7 11 13>;说头(@a); #1说尾巴(@a); #[3 5 7 11 13]

Optional and Default Arguments

By default all positional parameters require arguments. A question mark, ?, after a parameter marks it as optional so that you don’t need to supply an argument. This subroutine takes one or two arguments:

默认情况下,所有位置参数都需要参问号?后面的参数将其标记为可选,这样您就不需要提供参数。该子例程需要一个或两个参数:

sub one-or-two ( $a, $b? ) {
    put $b.defined ?? "Got $a and $b" !! "Got $a";
    }

one-or-two( 'Hamadryas' );
one-or-two( 'Hamadryas', 'perlicus' );

If you have an optional argument you probably want a default value. Assign to a parameter to give it a default value. That assignment occurs only when you don’t supply an argument:

如果您有可选参数,则可能需要默认值。分配给参数以为其提供默认值。只有在您不提供参数时才会发生该分配:

sub one-or-two ( $a, $b = 137 ) {
    put $b.defined ?? "Got $a and $b" !! "Got $a";
    }

one-or-two( 19 );                      # one number
one-or-two( 'Hamadryas', 'perlicus' ); # two strings
one-or-two( <Hamadryas perlicus> );    # one array
one-or-two( |<Hamadryas perlicus> );   # flattened array

The output shows that the arguments fill in the parameters differently each time:

输出显示参数每次填充参数的方式不同:

Got 19 and 137
Got Hamadryas and perlicus
Got Hamadryas perlicus and 137
Got Hamadryas and perlicus

You can’t have required positional parameters after an optional one:

在可选项之后,您不能拥有所需的位置参数:

sub one-or-two ( $a?, $b ) {
    put $b.defined ?? "Got $a and $b" !! "Got $a";
    }

That’s a compile-time error:

这是一个编译时错误:

Error while compiling
Cannot put required parameter $b after optional parameters

Parameter Traits

The parameter variables are filled in with read-only aliases to the original data. You see the same values but you can’t change them. This subroutine tries to add one to its value:

参数变量用原始数据的只读别名填充。您看到相同的值但无法更改它们。此子例程尝试在其值中添加一个:

sub increment ( $a ) { $a++ }

my $a = 137;
put increment( $a );

This doesn’t work because you can’t change the parameter variable:

这不起作用,因为您无法更改参数变量:

Cannot resolve caller postfix:<++>(Int); the following candidates
match the type but require mutable arguments:

The read-only alias is the default. You can change that by applying traits to the parameters. Apply the is copytrait to get a mutable value that’s separate from the original argument. You can change it without changing the original value:

只读别名是默认值。您可以通过将特征应用于参数来更改它。应用is copytrait以获取与原始参数分开的可变值。您可以在不更改原始值的情况下进行更改:

sub mutable-copy ( $a is copy ) { $a++; put "Inside: $a" }

my $a = 137;

put "Before: $a";
mutable-copy( $a );
put "After: $a";

The output shows that the original variable’s value did not change:

输出显示原始变量的值未更改:

Before: 137
Inside: 138
After: 137

Use the is rw trait to change the original value. If the argument is a writable container you can change the value. If the value is not some sort of container you’ll get an error:

使用is rw trait更改原始值。如果参数是可写容器,则可以更改该值。如果该值不是某种容器,则会出现错误:

sub read-write ( $a is rw ) { $a++ }

my $a  = 137;
my $b :=  37;
my \c  =   7;

read-write( $a );  # writable so okay
read-write( $b );  # literal,  not mutable - ERROR!
read-write( c );   # constant, not mutable - ERROR!
read-write( 5 );   # literal,  not mutable - ERROR!

参数约束

You can constrain a parameter to a particular type. You already saw some of this in Chapter 5:

您可以将参数约束为特定类型。您已经在第5章中看到了一些内容:

sub do-something ( Int:D $n ) { ... }

The sigils impose their own constraints. An @ accepts something that is a Positional, the % accepts something that does Associative, and the & accepts something that does Callable:

这些印记强加了自己的约束。 @接受一个定位的东西,%接受一个做关联的东西,而接受一些做Callable的东西:

sub wants-pos   ( @array ) { put "Got a positional: @array[]" }
sub wants-assoc ( %hash )  { put "Got an associative: {%hash.gist}" }
sub wants-code  ( &code )  { put "Got code" }

wants-pos( <a b c> );
wants-assoc( Map.new: 'a' => 1 );
wants-code( { put "Mini code" } );

These won’t work because they don’t supply the right types of arguments:

这些不起作用,因为它们不提供正确类型的参数:

wants-pos( %hash );
wants-assoc( <x y z> );
wants-code( 1 );

Additionally, something that accepts a code block can specify its own signature that must match the argument’s signature. Put the desired signature after the parameter variable:

此外,接受代码块的东西可以指定自己的签名,该签名必须与参数的签名匹配。在参数变量后面放置所需的签名:

sub one-arg  ( &code:( $a ), $A )         { &code.($A) }
sub two-args ( &code:( $a, $b ), $A, $B ) { &code.($A, $B) }

one-arg( { put "Got $^a" }, 'Hamadryas' );

two-args( { put "Got $^a and $^b" }, 'Hamadryas', 'perlicus' );

Same Name, Different Signature

You can define the same subroutine name twice by giving it different signatures. Each of these is a candidate. A dispatcher decides which candidate to call based on your arguments. There are several things the dispatcher considers, in this order:

您可以通过为其指定不同的签名来定义相同的子例程名称两次。这些都是候选人。调度员根据您的参数决定调用哪个候选者。调度员按以下顺序考虑以下几点:

  1. Literal value
  2. Number of arguments (arity)
  3. Types of arguments
  4. Other constraints

To define candidates, declare the subroutine with multi. And since multi works on a subroutine by default (you’ll see methods in Chapter 12), you can leave off the sub:

要定义候选项,请使用multi声明子例程。并且由于默认情况下多个工作在子程序上(您将在第12章中看到方法),您可以不使用子工具:

multi sub some-subroutine { ... }
multi some-subroutine { ... }

Literal Value Parameters

You can also make a signature that has a literal value. These multis are selected when the argument value is the same as the literal parameter:

您还可以创建具有文字值的签名。当参数值与文字参数相同时,选择这些multis:

multi something (  1 ) { put "Got a one" }
multi something (  0 ) { put "Got a zero" }
multi something ( $a ) { put "Got something else" }

something(   1 );
something(   0 );
something( 137 );

The literal value parameters decide the appropriate subroutine for the first two cases:

文字值参数决定前两种情况的相应子例程:

Got a one
Got a zero
Got something else

What if you wanted a Rat as one of the literal values? Put the value inside <> so the compiler doesn’t think the / is the start of a regex (Chapter 15):

如果您想将鼠作为字面值之一,该怎么办?将值放在<>中,这样编译器就不会认为/是正则表达式的开头(第15章):

multi something ( 1 )       { put "Got a one" }
multi something ( 0 )       { put "Got a zero" }
multi something ( <1/137> ) { put "Got something fine" }
multi something ( $b )      { put "Got something else" }

something( 1 );
something( 0 );
something( 1/137 );
something( 'Hello' );

Think about the previous Fibonacci example:

想想之前的Fibonacci示例:

sub fibonacci ( $n ) {
    return 0 if $n == 0;
    return 1 if $n == 1;
    return &?ROUTINE( $n - 1 ) + &?ROUTINE( $n - 2 );
    }

That implementation has two special cases for 0 and 1. You have to provide special code to handle those. You can move those special cases away from the main idea by giving each case its own multi:

该实现有两个特殊情况0和1.您必须提供特殊代码来处理这些。您可以通过为每个案例提供自己的多个来移动这些特殊情况远离主要想法:

multi fibonacci ( 0 ) { 0 }
multi fibonacci ( 1 ) { 1 }

multi fibonacci ( $n ) {
    return fibonacci( $n - 1 ) + fibonacci( $n - 2 );
    }

put fibonacci(0);
put fibonacci(1);
put fibonacci(5);

Notice that you can’t use &?ROUTINE because $n-1 might not be handled by the same subroutine.

请注意,您不能使用&?ROUTINE,因为$ n-1可能无法由同一子例程处理。

Number of Arguments

Declare the sub with multi. One candidate takes a single positional argument and the other candidate takes two positional arguments:

用multi声明sub。一个候选者采用单个位置参数,另一个候选者采用两个位置参数:

multi subsomething ( $a     ) { put "One argument"; }
multi subsomething ( $a, $b ) { put "Two arguments"; }

something( 1 );
something( 1, 3 );
# something();

The output shows that you called two different subroutines:

输出显示您调用了两个不同的子例程:

One argument
Two arguments

Uncomment the call with no arguments, and you’ll get a compile-time error. The compiler knows no signatures can match:

取消注释没有参数的调用,你将得到一个编译时错误。编译器知道没有签名可以匹配:

Calling something() will never work with any of these multi signatures:
    ($a)
    ($a, $b)

You can shorten the multi sub to simply multi since that implies sub:

你可以将multi sub简化为multi,因为这意味着sub:

multi something ( $a     ) { put "One argument";  }
multi something ( $a, $b ) { put "Two arguments"; }

This sort of dispatch depends on arity—the number of arguments that you supply. This means that the compiler also knows when you try to define subroutines with the same arity, like this:

这种调度取决于arity - 您提供的参数数量。这意味着编译器也知道您何时尝试使用相同的arity定义子例程,如下所示:

multi something ( $a ) { put "One argument"; }
multi something ( $b ) { put "Also one arguments"; } # Error

This is also a runtime error because the dispatcher can’t choose one candidate over the other (and it won’t run all of them):

这也是一个运行时错误,因为调度程序不能选择一个候选项而不是另一个候选项(它不会运行所有这些候选项):

Ambiguous call to 'something'; these signatures all match:
:($a)
:($b)

Parameter Types

You can also choose amongst multis by parameter type. These each take the same number of arguments but distinguish them by type:

您还可以通过参数类型在multis中进行选择。这些参数都采用相同数量的参数,但按类型区分:

multi something ( Int:D $a ) { put "Int argument";  }
multi something ( Str:D $a ) { put "Str arguments"; }

something( 137 );
something( 'Hamadryas' );

These call different subroutines because the argument types are different:

这些调用不同的子例程,因为参数类型不同:

Int argument
Str arguments

You might have the different subroutines take the same type. In those cases you can select the right one by a custom constraint. The dispatcher chooses the most specific one:

您可能有不同的子例程采用相同的类型。在这些情况下,您可以通过自定义约束选择正确的约束。调度员选择最具体的一个:

multi something ( Int:D $a ) { put "Odd arguments"; }
multi something ( Int:D $a where * %% 2 ) { put "Even argument" }

something( 137 );
something( 538 );

Notice that this works regardless of the order in which you define the subroutines:

请注意,无论您定义子例程的顺序如何,这都有效:

Odd arguments
Even arguments

In the next example the first subroutine constrains its parameter to numbers that are odd. The second subroutine constrains its parameter to numbers greater than 5. These both have one parameter and they both have a whereclause, so the dispatcher chooses the first one it encounters:

在下一个示例中,第一个子例程将其参数约束为奇数。第二个子例程将其参数约束为大于5的数字。这两个参数都有一个参数,它们都有一个whereclause,所以调度程序选择它遇到的第一个参数:

multi sub something ( Int:D $a where * % 2 ) { put "Odd number" }
multi sub something ( Int:D $a where * > 5 ) { put "Greater than 5" }

something( 137 );

The argument satisfies either signature. The output shows that the first subroutine ran:

该论点满足任一签名。输出显示第一个子例程运行:

Odd number

Reverse the order of definition:

颠倒定义的顺序:

multi sub something ( Int:D $a where * > 5 ) { put "Greater than 5" }
multi sub something ( Int:D $a where * % 2 ) { put "Odd number" }

something( 137 );

The first defined subroutine still runs even though it’s a different definition:

第一个定义的子例程仍然运行,即使它是一个不同的定义:

Greater than 5

What if you do not want multiple definitions with the same name? Declare one of the subroutines withoutmulti:

如果您不希望使用相同名称的多个定义,该怎么办?声明一个没有多个子程序的子程序:

sub something ( Int $a ) { put "Odd arguments" }

multi something ( Int $a where * %% 2 ) { # redefinition!
    put "Even argument";
    }

You get a compile-time error asking if you meant that to be a multi sub:

你得到一个编译时错误,询问你是否认为这是一个多子:

===SORRY!=== Error while compiling
Redeclaration of routine 'something' (did you mean to declare a multi-sub?)

Named Parameters

Named parameters do not depend on their position in the parameter or argument lists. By default they are optional. You can specify them anywhere in the arguments and in any order. These are often used to set options for a routine or method.

命名参数不依赖于它们在参数或参数列表中的位置。默认情况下,它们是可选的您可以在参数中的任何位置以任何顺序指定它们。这些通常用于设置例程或方法的选项。

Specify named parameters with a colon before the parameter variable. In the signature, use the unquoted parameter variable name, the fat arrow, and the value that you want to supply. The order of the names or values does not matter:

在参数变量之前使用冒号指定命名参数。在签名中,使用不带引号的参数变量名称,胖箭头和要提供的值。名称或值的顺序无关紧要:

sub add ( Int:D :$a, Int:D :$b ) {
    $a + $b;
    }

put add( a => 1,  b => 36 );  # 37
put add( b => 36, a => 1  );  # Same thing

For this to work you cannot quote the keys or use variables as the keys. This call is actually two Pair objects treated as positional parameters:

为此,您无法引用键或使用变量作为键。这个调用实际上是两个被视为位置参数的Pair对象:

put add( 'a' => 1,  'b' => 36 );     # Will not work!
put add( $keya => 1, $keyb => 36 );  # Will not work!

More often you’ll use the adverb syntax. With values that are positive integers you can specify the value first and the name after it:

更常见的是,您将使用副词语法。对于正整数值,您可以先指定值,然后指定其后的名称:

put add( :a(1), :b(36) );  # 37
put add( :36b, :1a );      # 37

Default values and other constraints work the same as they do with positional parameters:

默认值和其他约束与位置参数的作用相同:

sub add ( Int:D :$a = 0, Int:D :$b = 0 ) {
    $a + $b;
    }

put add();       # 0
put add( :36b ); # 36

You don’t have to use the same names for the arguments and the parameter variables. In complicated code in power-of you might not want to retype $base or $power every time. The subroutine still uses the long names for the interface but the implementation can use the short names:

您不必对参数和参数变量使用相同的名称。在功能复杂的代码中,您可能不希望每次都重新键入$ base或$ power。子例程仍然使用接口的长名称,但实现可以使用短名称:

sub power-of ( Int:D :power($n) = 0, Int:D :base($a) ) {
    $a ** $n
    }

put power-of( base => 2, power => 5 ); # 32

So far these named parameters have all taken values. Without any other constraints and no argument value, a named parameter is a Boolean. The adverb form with no value (and no constraint) gets True (because that’s what Pairs do):

到目前为止,这些命名参数都采用了值。没有任何其他约束和参数值,命名参数是布尔值。没有值(并且没有约束)的副词形式为True(因为这就是Pairs所做的):

sub any-args ( :$state ) { say $state  }
any-args( :state );  #  True

A ! in front of the adverb name makes it a False value:

一个 !在副词名称前面使其成为一个假值:

any-args( :!state );  #  False

Required Named Parameters

A positional parameter is required simply because it exists, and you have to mark it as optional to make it such. That’s reversed with named parameters, which are optional unless you say otherwise. The parameters get their default values if you don’t specify them:

仅需要位置参数因为它存在,并且您必须将其标记为可选,以使其成为可能。这与命名参数相反,除非您另有说明,否则这些参数是可选的。如果您不指定参数,参数将获得其默认值:

sub not-required ( :$option ) { say $option; }

not-required();            # (Any)
not-required( :option  );  # True
not-required( :!option );  # False
not-required( :5option );  # 5

To make option mandatory put a ! after it in the signature (this is not the same as ! before an argument):

要强制选择放一个!在签名之后(这与参数之前的!不一样):

sub not-required ( :$option! ) { say $option; }

not-required();            # Error!

The error tells you that you forgot an argument:

该错误告诉您忘记了一个参数:

Required named parameter 'option' not passed

Named Parameters for Free

Rather than define every named parameter, you can accept all of them. Don’t specify any in your parameters and they all show up in %_. This is the equivalent of @_ but for named parameters. Each routine gets its own version of this variable:

您可以接受所有参数,而不是定义每个命名参数。不要在参数中指定任何参数,它们都显示在%_中。这相当于@_但是对于命名参数。每个例程都有自己的变量版本:

sub any-args { say %_ }
any-args( genus => 'Hamadryas' );
any-args( genus => 'Hamadryas', species => 'perlicus' );

You didn’t define either :genus or :species but they show up in %_:

你没有定义:genus或:species但是它们出现在%_中:

{genus => Hamadryas}
{genus => Hamadryas, species => perlicus}

A slurpy Hash does the same thing:

一个邋H的哈希做同样的事情:

sub any-args ( *%args ) { say %args }
any-args( genus => 'Hamadryas' );
any-args( genus => 'Hamadryas', species => 'perlicus' );

That’s how that implicit %_ worked. When you use it in a subroutine you automatically get a slurpy for it in the signature:

这就是隐含的%_的工作方式。当您在子例程中使用它时,您会在签名中自动获取它:

sub any-args { say %_ }
sub any-args ( *%_ ) { say %_ }

Mixed Parameters

You can mix positional and named parameters. If you use @_ and %_ in the code they are both in the implicit signature:

您可以混合位置和命名参数。如果在代码中使用@和%,则它们都在隐式签名中:

sub any-args {
    put '@_ => ', @_.gist;
    put '%_ => ', %_.gist;
    }

any-args( 'Hamadryas', 137, :status, :color('Purple') );

@_ => [Hamadryas 137]
%_ => {color => Purple, status => True}

You can mix in the named parameters in any order that you like. The positional parameters have to be in the right order but named parameters can come between them:

您可以按您喜欢的任何顺序混合命名参数。位置参数必须是正确的顺序,但命名参数可以介于它们之间:

any-args( :color('Purple'), 'Hamadryas', :status, 137  );

It’s the same if you name the parameters yourself:

如果您自己命名参数,则相同:

sub any-args ( *@args, *%named ) {
    put '@args => ', @args.gist;
    put '%named => ', %named.gist;
    }

any-args( :color('Purple'), 'Hamadryas', :status, 137  );

Return Types

You can constrain the return values of subroutines. If you try to return a value that doesn’t fit the restriction you get a runtime Exception. Specify the type after the signature with a -->. You want this subroutine to return a defined Int:

您可以约束子例程的返回值。如果您尝试返回不符合限制的值,则会获得运行时异常。使用 - >指定签名后的类型。您希望此子例程返回已定义的Int:

sub returns-an-int ( Int:D $a, Int:D $b --> Int:D ) { $a + $b }

put returns-an-int( 1, 3 );

That works:

这样可行:

4

But what if you make a mistake where you return a Str?

但是,如果你在返回Str时犯了错误怎么办?

sub returns-an-int ( Int:D $a, Int:D $b --> Int:D ) { ($a + $b).Str }

put returns-an-int( 1, 3 );

At runtime you get an error because the types do not match:

在运行时,您会收到错误,因为类型不匹配:

Type check failed for return value; expected Int but got Str ("4")

An alternate way is to note it with returns (with an s at the end) outside of the signature’s parentheses:

另一种方法是在签名的括号外面用返回(在末尾有一个s)注意它:

sub returns-an-int ( Int $a, Int $b ) returns Int { $a + $b }

You might also see these forms that do the same thing:

您可能还会看到这些表单执行相同的操作:

sub returns-an-int ( Int $a, Int $b ) of Int { $a + $b }

my Int sub returns-an-int ( Int $a, Int $b ) { $a + $b }

No matter which way you define the return type you can always return either Nil or a Failure object (usually to signal that something went wrong). All of these calls “succeed” even though some of them don’t return a Str:

无论您以何种方式定义返回类型,都可以始终返回Nil或Failure对象(通常表示出现问题)。所有这些调用都“成功”,即使其中一些不返回Str:

sub does-not-work ( Int:D $a --> Str ) {
    return Nil if $a == 37;
    fail 'Is not a fine number' unless $a == 137;
    return 'Worked!'
    }

put does-not-work(  37 ).^name;   # Nil
put does-not-work( 137 ).^name;   # Str
put does-not-work( 538 ).^name;   # Failure

You can’t make complex checks in the constraint, but you can define a subset that does these. Here’s one that returns either a Rat or, if you try to divide by zero, an Inf:

您不能在约束中进行复杂检查,但可以定义执行这些检查的子集。这里有一个返回Rat或者,如果你试图除以零,一个Inf:

subset RatInf where Rat:D | Inf;

sub divide ( Int:D $a, Int:D $b --> RatInf ) {
    return Inf if $b == 0;
    $a / $b;
    }

put divide( 1, 3 );  # <1/3>
put divide( 1, 0 );  # Inf

That Rat:D | Inf is a Junction. You’ll see those in Chapter 14.

Summary

Much of the work of ensuring your program does the right things can be done with the judicious use of constraints on the inputs and outputs of subroutines. With a little planning these features will catch the cases that you did not expect and that shouldn’t show up in your program. Once they’ve been found you can work your way through the code to find them even sooner—and the sooner you find them, the easier your debugging life should be.

确保您的程序做正确的事情的大部分工作可以通过明智地使用子程序的输入和输出的约束来完成。通过一些计划,这些功能将捕获您不期望的并且不应该出现在您的程序中的情况。一旦找到它们,您就可以通过代码更快地找到它们 - 并且越早找到它们,您的调试生活就越容易。

comments powered by Disqus