Functions and Functional Programming in Perl 6
例程(Routines)是 Perl 6 中代码重用的最小手段。它们有几种形式,最明显的是属于类和角色并与对象相关联的方法,还有函数, 也叫做子例程或短子程序,它们独立于对象而存在。
子例程默认是词法(my)作用域的,对它们的调用通常在编译时解决。
子例程可以具有签名,也称为参数列表,其指定签名期望的参数(如果有的话)。 它可以指定(或保持打开)参数的数量和类型,以及返回值。
子例程的内省通过例程提供。
定义/创建/使用 函数
子例程
要创建一个函数,通常所需要的是使用 sub
声明符定义一个子例程:
sub my-func { say "Look ma, no args!" }
my-func;
为了使子程序接受参数,签名被放置在子例程名称和它的函数主体之间,在括号中:
sub exclaim ($phrase) {
say $phrase ~ "!!!!"
}
exclaim "Howdy, World";
默认地, 子例程是词法作用域的。即 sub foo {...}
和 my sub foo {...}
是相同的并且只被定义在当前作用域中。
sub escape($str) {
# Puts a slash before non-alphanumeric characters
S:g[<-alpha -digit>] = "\\$/" given $str
}
say escape 'foo#bar?'; # foo\#bar\?
{
sub escape($str) {
# Writes each non-alphanumeric character in its hexadecimal escape
S:g[<-alpha -digit>] = "\\x[{ $/.ord.base(16) }]" given $str
}
say escape 'foo#bar?' # foo\x[23]bar\x[3F]
}
# Back to original escape function
say escape 'foo#bar?'; # foo\#bar\?
子例程不必命名; 这种情况下, 它们被叫做匿名的。
say sub ($a, $b) { $a ** 2 + $b ** 2 }(3, 4) # 25
但在这种情况下,通常希望使用更简洁的块语法。可以就地调用子例程和块,如上例所示。
Blocks 和 Lambdas
每当你看到像
{ $_ + 42 }, -> $a, $b { $a ** $b }
或
{ $^text.indent($:spaces) }
那么这是块语法。 它在每个 if
,for
,while
等关键字之后使用。
它们也可以作为匿名代码块自己使用。
say { $^a ** 2 + $^b ** 2}(3, 4) # 25
有关块语法的详细信息,请参阅块类型的文档。
签名
函数接受的参数在其签名中有描述。
sub format(Str $s) { ... }
-> $a, $b { ... }
有关签名的语法和使用的详细信息,请参阅 Signature 类的文档。
自动签名
如果没有提供签名,但在函数体中使用了两个自动变量 @_
或 %_
,则将生成带有 *@_
或 *%_
的签名。 两个自动变量可以同时使用。
sub s { dd @_, %_ };
dd &s.signature # OUTPUT«:(*@_, *%_)»
参数
参数以逗号分隔列表的形式提供。 要消除嵌套调用的歧义, 可以使用圆括号或副词形式。
sub f(&c){ c() * 2 }; # call the function reference c with empty parameter list
sub g($p){ $p - 2 };
say(g(42)); # nest call to g in call to say
f: { say g(666) }; # call f with a block
当调用函数时,位置参数应该以与函数签名相同的顺序提供。 命名参数可以以任何顺序提供,但是最好将命名参数放在位置参数之后。 在函数调用的参数列表中,支持一些特殊的语法:
sub f(|c){};
f :named(35); # 具名参数(in "adverb" form.)
f named => 35; # 也是具名参数.
f :35named; # 使用缩写的副词形式的具名参数
f 'named' => 35; # 不是具名参数, 而是一个 Pair 位置参数
my \c = <a b c>.Capture;
f |c; # Merge the contents of Capture $c as if they were supplied
传递给函数的参数在概念上首先被收集在 Capture 容器中。 关于这些容器的语法和使用的细节可以在 Capture 类的文档中找到。
当使用命名参数时,请注意,正常的 List "pair-chaining" 允许在命名参数之间跳过逗号。
sub f(|c){};
f :dest</tmp/foo> :src</tmp/bar> :lines(512);
f :32x :50y :110z; # This flavor of "adverb" works, too
f :a:b:c; # The spaces are also optional.
返回值
任何块或例程将把它的最后一个表达式作为返回值提供给调用者。如果 return 或 return-rw 被调用,它们的参数(如果有的话)将成为返回值。 默认返回值为 Nil。
sub a { 42 };
sub b { say a };
b;
# OUTPUT«42»
多个返回值作为列表或通过创建捕获返回。 解构可以用于解开多个返回值。
sub a { 42, 'answer' };
put a.perl;
# OUTPUT«(42, "answer")»
my ($n, $s) = a;
put [$s, $n];
# OUTPUT«answer 42»
sub b { <a b c>.Capture };
put b.perl;
# OUTPUT«\("a", "b", "c")»
返回类型约束
Perl 6 有很多方式来指定函数的返回类型:
sub foo(--> Int) {}; say &foo.returns; # (Int)
sub foo() returns Int {}; say &foo.returns; # (Int)
sub foo() of Int {}; say &foo.returns; # (Int)
my Int sub foo() {}; say &foo.returns; # (Int)
尝试返回另外一种类型的值会引起编译错误。
sub foo() returns Int { "a"; }; foo; # Type check fails
注意,Nil
和 Failure
是免于返回类型约束,并且可以从任何子例程返回,而不管其约束:
sub foo() returns Int { fail }; foo; # Failure returned
sub bar() returns Int { return }; bar; # Nil returned
多重分派
Perl 6 允许你使用同一个名字但是不同签名写出几个子例程。当子例程按名字被调用时, 运行时环境决定哪一个子例程是最佳匹配, 然后调用那个候选者。你使用 multi
声明符来声明每个候选者。
multi congratulate($name) {
say "祝你生日快乐, $name";
}
multi congratulate($name, $age) {
say "祝 $age 岁生日快乐, $name";
}
congratulate 'Camelia'; # 祝你生日快乐, Camelia
congratulate 'Rakudo', 15; # 祝你 15 岁生日快乐, Rakudo
分发/分派(dispatch) 可以发生在参数的数量(元数)上, 但是也能发生在类型上:
multi as-json(Bool $d) { $d ?? 'true' !! 'false' }
multi as-json(Real $d) { ~$d }
multi as-json(@d) { sprintf '[%s]', @d.map(&as-json).join(', ') }
say as-json([True, 42]); # [true, 42]
不带任何指定例程类型的 multi
总是默认为 sub
, 但是你也可以把 multi
用在方法(methods)上。那些候选者全都是对象的 multi
方法:
class Congrats {
multi method congratulate($reason, $name) {
say "Hooray for your $reason, $name";
}
}
role BirthdayCongrats {
multi method congratulate('birthday', $name) {
say "Happy birthday, $name";
}
multi method congratulate('birthday', $name, $age) {
say "Happy {$age}th birthday, $name";
}
}
my $congrats = Congrats.new does BirthdayCongrats;
$congrats.congratulate('升职', 'Cindy'); #-> 恭喜你升职,Cindy
$congrats.congratulate('birthday', 'Bob'); #-> Happy birthday, Bob
proto
proto 从形式上声明了 multi
候选者之间的共性
。 proto 充当作能检查但不会修改参数的包装器。看看这个基本的例子:
proto congratulate(Str $reason, Str $name, |) {*}
multi congratulate($reason, $name) {
say "Hooray for your $reason, $name";
}
multi congratulate($reason, $name, Int $rank) {
say "Hooray for your $reason, $name -- you got rank $rank!";
}
congratulate('being a cool number', 'Fred'); # OK
congratulate('being a cool number', 'Fred', 42); # OK
congratulate('being a cool number', 42); # Proto match error
所有的 multi congratulate
都会遵守基本的签名, 这个签名中有两个字符串参数, 后面跟着可选的更多的参数。 |
是一个未命名的 Capture
形参, 它允许 multi
接收额外的参数。第三个 congratulate 调用在编译时失败, 因为第一行的 proto 的签名变成了所有三个 multi congratulate 的共同签名, 而 42 不匹配 Str
。
say &congratulate.signature #-> (Str $reason, Str $name, | is raw)
你可以给 proto
一个函数体, 并且在你想执行 dispatch 的地方放上一个 {*}
。
# attempts to notify someone -- returns False if unsuccessful
proto notify(Str $user,Str $msg) {
my \hour = DateTime.now.hour;
if hour > 8 or hour < 22 {
return {*};
} else {
# we can't notify someone when they might be sleeping
return False;
}
}
{*}
总是分派给带有参数的候选者。默认参数和类型强制转换会起作用单不会传递。
proto mistake-proto(Str() $str, Int $number = 42) {*}
multi mistake-proto($str,$number) { say $str.WHAT }
mistake-proto(7,42); #-> (Int) -- coercions not passed on
mistake-proto('test'); #!> fails -- defaults not passed on
约定和惯用法
虽然上面描述的调度系统提供了很多灵活性,但是存在一些大多数内部函数以及许多模块中的函数将遵循的约定。 这些将产生一致的外观和感觉。
吞噬约定
也许最重要的是处理 slurpy 列表参数的方式。 大多数时候,函数不会自动展平吞噬(slurpy)列表。 罕见的例外是在列表的列表上没有合理行为的那些函数(例如chrs),或者与已建立的习语有冲突的函数,例如 pop 是 push 的逆操作。
如果你想匹配这个外观和感觉,任何可迭代(Iterable)参数必须使用 **@slurpy
逐个元素地打开,有两个细微差别:
- Scalar 容器内的 Iterable 不计数。
- 在顶层使用
,
创建的列表只能计数为一个 Iterable。
这可以通过使用带有 +
或 +@
而不是 **
的 slurpy 来实现:
sub grab(+@a) { "grab $_".say for @a }
这非常接近于:
multi sub grab(**@a) { "grab $_".say for @a }
multi sub grab(\a) {
a ~~ Iterable and a.VAR !~~ Scalar ?? nextwith(|a) !! nextwith(a,)
}
这导致以下行为,称为「单参数规则」,并且理解什么时间调用 slurpy 函数很重要:
grab(1, 2); # grab 1 grab 2
grab((1, 2)); # grab 1 grab 2
grab($(1, 2)); # grab 1 2
grab((1, 2), 3); # grab 1 2 grab 3
这也使得用户请求的展平感觉一致,无论有没有子列表,或很多
grab(flat (1, 2), (3, 4)); # grab 1 grab 2 grab 3 grab 4
grab(flat $(1, 2), $(3, 4)); # grab 1 2 grab 3 4
grab(flat (1, 2)); # grab 1 grab 2
grab(flat $(1, 2)); # grab 1 2
值得注意的是,在这些情况下将绑定和无符号变量混合在一起需要一点技巧,因为在绑定期间没有使用 Scalar 中间人。
my $a = (1, 2); # Normal assignment, equivalent to $(1, 2)
grab($a); # grab 1 2
my $b := (1, 2); # Binding, $b links directly to a bare (1, 2)
grab($b); # grab 1 grab 2
my \c = (1, 2); # Sigilless variables always bind, even with '='
grab(c); # grab 1 grab 2
函数是一等对象
函数和其他代码对象可以作为值传递,就像任何其他对象一样。
有几种方法来获取代码对象。 您可以在声明点将其赋值给变量:
my $square = sub (Numeric $x) { $x * $x }
# and then use it:
say $square(6); # 36
或者,您可以通过使用它前面的 &
来引用现有的具名函数。
sub square($x) { $x * $x };
# get hold of a reference to the function:
my $func = &square
这对于高阶函数非常有用,即,将其他函数作为输入的函数。 一个简单高阶函数的是 map,它对每个输入元素应用一个函数:
sub square($x) { $x * $x };
my @squared = map &square, 1..5;
say join ', ', @squared; # 1, 4, 9, 16, 25
中缀形式
要像中缀运算符那样调用具有2个参数的子例程,请使用由 [
和 ]
包围的子例程引用。
sub plus { $^a + $^b };
say 21 [&plus] 21;
# OUTPUT«42»
闭包
Perl 6 中的所有代码对象都是闭包,这意味着它们可以从外部作用域引用词法变量。
sub generate-sub($x) {
my $y = 2 * $x;
return sub { say $y };
# ^^^^^^^^^^^^^^ inner sub, uses $y
}
my $generated = generate-sub(21);
$generated(); # 42
这里 $y
是 generate-sub
中的词法变量,并且返回的内部子例程使用了 $y
。 到内部 sub 被调用时,generate-sub
已经退出。 然而内部 sub 仍然可以使用 $y
,因为它关闭了变量。
一个不太明显但有用的闭包示例是使用 map 乘以数字列表:
my $multiply-by = 5;
say join ', ', map { $_ * $multiply-by }, 1..5; # 5, 10, 15, 20, 25
这里传递给 map
的块从外部作用域引用变量 $multiply-by
,使块成为闭包。
没有闭包的语言不能轻易地提供高阶函数,它们像 map 一样易于使用和强大。
Routines
例程是遵守 Routine 类型的代码对象,最明显的是 Sub,方法,正则表达式和Submethod。
他们携带除了块提供的额外的功能:他们可以作为 multis,你可以包装它们,并使用 return
提前退出:
my $keywords = set <if for unless while>;
sub has-keyword(*@words) {
for @words -> $word {
return True if $word (elem) $keywords;
}
False;
}
say has-keyword 'not', 'one', 'here'; # False
say has-keyword 'but', 'here', 'for'; # True
这里 return
不仅仅是将离开它所调用的块的内部,而是离开整个程序。 一般来说,块对于 return
是透明的,它们附加到外部程序。
例程(Routines)可以是内联的,并且因此为包装设置了障碍。 使用指令 use soft;
以防止内联在运行时允许包装。
sub testee(Int $i, Str $s){
rand.Rat * $i ~ $s;
}
sub wrap-to-debug(&c){
say "wrapping {&c.name} with arguments {&c.signature.perl}";
&c.wrap: sub (|args){
note "calling {&c.name} with {args.gist}";
my \ret-val := callwith(|args);
note "returned from {&c.name} with return value {ret-val.perl}";
ret-val
}
}
my $testee-handler = wrap-to-debug(&testee);
# OUTPUT«wrapping testee with arguments :(Int $i, Str $s)»
say testee(10, "ten");
# OUTPUT«calling testee with \(10, "ten")
returned from testee with return value "6.151190ten"
6.151190ten»
&testee.unwrap($testee-handler);
say testee(10, "ten");
# OUTPUT«6.151190ten»
定义操作符
操作符只是有趣名字的子例程。 有趣的名称由类别名称(中缀,前缀,后缀,环缀,后环缀)组成,后面跟着冒号,以及一个或多个操作符名称的列表(在环缀和后环缀的情况下为两个组件)。
这既适用于向现有运算符添加多个候选项,也适用于定义新的运算符。 在后一种情况下,新子例程的定义自动将新运算符安装到 语法(grammar)中,但仅在当前词法作用域中。 通过 use
或 import
导入操作符也使其可用。
# adding a multi candidate to an existing operator:
multi infix:<+>(Int $x, "same") { 2 * $x };
say 21 + "same"; # 42
# 定义一个新的操作符
sub postfix:<!>(Int $x where { $x >= 0 }) { [*] 1..$x };
say 6!; # 720
运算符声明变得尽快可用,因此您甚至可以递归到刚才定义的运算符中,如果您真的想要:
sub postfix:<!>(Int $x where { $x >= 0 }) {
$x == 0 ?? 1 !! $x * ($x - 1)!
}
say 6!; # 720
环缀和后环缀操作符由两个分隔符组成,一个开口和一个闭合。
sub circumfix:<START END>(*@elems) {
"start", @elems, "end"
}
say START 'a', 'b', 'c' END; # start a b c end
后环缀也接收这个术语,在它们被作为参数解析之后:
sub postcircumfix:<!! !!>($left, $inside) {
"$left -> ( $inside )"
}
say 42!! 1 !!; # 42 -> ( 1 )
块可以直接赋值给操作符名。 使用变量声明符,并在操作符名前加上一个 &
符号。
my &infix:<ieq> = -> |l { [eq] l>>.fc };
say "abc" ieq "Abc";
# OUTPUT«True»
优先级
Perl 6 中的运算符优先级相对于现有运算符指定。 is tighter
、is equiv
和 is looser
特性能使用一个运算符提供,新的运算符优先级与之相关。 可以应用更多的特征。
例如,infix:<*>
的优先级高于 infix:<+>
,并且在中间挤压一个像这样:
sub infix:<!!>($a, $b) is tighter(&infix:<+>) {
2 * ($a + $b)
}
say 1 + 2 * 3 !! 4; # 21
这里 1 + 2 * 3 !! 4
被解析为 1 + ((2 * 3) !! 4)
,因为新的 !!
运算符的优先级在 +
和 *
之间。
可以使用下面的代码实现相同的效果:
sub infix:<!!>($a,$b) is looser(&infix:<x>) { ... }
要将新运算符置于与现有运算符相同的优先级别上,请使用 is equiv(&other-operator)
。
结合性
当同一个操作符在一行中连续出现多次时,有多种可能的解释。 例如
1 + 2 + 3
能被解析为
(1 + 2) + 3 # 左结合性
或者解析为
1 + (2 + 3) # 右结合性
对于实数的加法,区别有点模糊,因为 +
是数学上相关的。
但对其他运算符来说它很重要。 例如对于指数/幂运算符,infix:<**>
:
say 2 ** (2 ** 3); # 256
say (2 ** 2) ** 3; # 64
Perl 6 拥有以下可能的结合性配置:
A | Assoc | Meaning of $a ! $b ! $c |
---|---|---|
L | left | ($a ! $b) ! $c |
R | right | $a ! ($b ! $c) |
N | non | ILLEGAL |
C | chain | ($a ! $b) and ($b ! $c) |
X | list | infix:<!>($a; $b; $c) |
您可以使用 is assoc
trait 指定运算符的结合性,其中 left
是默认的结合性。
sub infix:<§>(*@a) is assoc<list> {
'(' ~ @a.join('|') ~ ')';
}
say 1 § 2 § 3; # (1|2|3)
Traits
特性(traits)是在编译时运行以修改类型,变量,例程,属性或其他语言对象的行为的子例程。
traits 的例子有:
class ChildClass is ParentClass { ... }
# ^^ trait, with argument ParentClass
has $.attrib is rw;
# ^^^^^ trait with name 'rw'
class SomeClass does AnotherRole { ... }
# ^^^^ trait
has $!another-attribute handles <close>;
# ^^^^^^^ trait
还有之前章节中的 is tighter
、is looser
、is equiv
、is assoc
等。
Traits 是 trait_mod<VERB>
形式的 subs, 其中 VERB
代表像 is
、does
、handles
那样的名字。它接受修改后的东西作为参数, 还有名字作为具名参数。
multi sub trait_mod:<is>(Routine $r, :$doubles!) {
$r.wrap({
2 * callsame;
});
}
sub square($x) is doubles {
$x * $x;
}
say square 3; # 18
请参阅内置常规性状文档的类型例程。
重新分派
在某些情况下,例程可能想从链中调用下一个方法。 这个链可以是类层次结构中的父类的列表,或者它可以是来自多分派的较不具体的 multi 候选者,或者它可以是来自wrap
的内部例程。
在所有这些情况下,您可以使用 callwith
通过您自己选择的参数调用链中的下一个例程。
multi a(Any $x) {
say "Any $x";
return 5;
}
multi a(Int $x) {
say "Int $x";
my $res = callwith($x + 1);
say "Back in Int with $res";
}
a 1;
# OUTPUT:
# Int 1
# Any 2
# Back in Int with 5
这里,a 1
首先调用最具体的 Int
候选者,并且 callwith
重新调度到较不具体的 Any
候选者。
通常,重新分派传递和调用者接收到的相同的参数,因此有一个特殊的例程:callsame
。
multi a(Any $x) {
say "Any $x";
return 5;
}
multi a(Int $x) {
say "Int $x";
my $res = callsame;
say "Back in Int with $res";
}
a 1; # Int 1\n Any 1\n Back in Int with 5
另一个常见的用例是重新分派到链中的下一个例程,之后不执行任何其他操作。 这就是为什么我们有 nextwith
和 nextsame
,它使用任意的参数调用下一个例程(nextwith
)或与调用者接收(nextsame
)相同的参数,但不会返回给调用者。 或者对其进行不同的措辞,nextsame
和 nextwith
变体用下一个候选项替换当前的调用帧(callframe)。
multi a(Any $x) {
say "Any $x";
return 5;
}
multi a(Int $x) {
say "Int $x";
nextsame;
say "back in a"; # never executed, because 'nextsame' doesn't return
}
a 1; # Int 1\n Any 1
如前所述,multi sub 不是唯一能在 call,call me,nextwith 和 next 中有帮助的情况。 下面是是调度到包装的例程:
# enable wrapping:
use soft;
# function to be wrapped:
sub square-root($x) { $x.sqrt }
&square-root.wrap(sub ($num) {
nextsame if $num >= 0;
1i * callwith(abs($num));
});
say square-root(4); # 2
say square-root(-4); # 0+2i
最后一个用例是从父类中重分派给方法。
class LoggedVersion is Version {
method new(|c) {
note "New version object created with arguments " ~ c.perl;
nextsame;
}
}
say LoggedVersion.new('1.0.2');
如果你需要对被包装的代码进行多次调用或获得一个引用,例如内省它,你可以使用 nextcallee
。
sub power-it($x) { $x * $x }
sub run-it-again-and-again($x) {
my &again = nextcallee;
again again $x;
}
&power-it.wrap(&run-it-again-and-again);
say power-it(5); # 625
强制类型
强制类型可以帮助您在例程中拥有特定类型,但接受更宽的输入。 当调用例程时,参数将自动转换为较窄的类型。
sub double(Int(Cool) $x) {
2 * $x
}
say double '21'; # 42
say double Any; # Type check failed in binding $x; expected 'Cool' but got 'Any'
这里的 Int
是参数将被强制的目标类型,而 Cool
是例程接受的作为输入的类型。
如果接受的输入类型为 Any
,则可以将 Int(Any)
缩写为 Int()
。
强制只需查找与目标类型具有相同名称的方法即可。 所以你可以为你自己的类型定义强制,像这样:
class Bar {...}
class Foo {
has $.msg = "I'm a foo!";
method Bar {
Bar.new(:msg($.msg ~ ' But I am now Bar.'));
}
}
class Bar {
has $.msg;
}
sub print-bar(Bar() $bar) {
say $bar.WHAT; # (Bar)
say $bar.msg; # I'm a foo! But I am now Bar.
}
print-bar Foo.new;
强制类型应该在类型工作的任何地方工作,但 Rakudo 当前(2015.02)仅针对子例程参数实现了它们。
sub MAIN
具有特殊名称 MAIN 的 sub 在所有相关 parsers 之后执行,并且其签名是可以解析命令行参数的装置。 支持 multi 方法,如果未提供命令行参数,则会自动生成并显示使用方法。 所有命令行参数在 @*ARGS 中也可用,它可以在被 MAIN 处理之前进行变换。
MAIN
的返回值被忽略。 要提供除 0 以外的退出代码,请调用 exit。
sub MAIN( Int :$length = 24,
:file($data) where { .IO.f // die "file not found in $*CWD" } = 'file.dat',
Bool :$verbose )
{
say $length if $length.defined;
say $data if $data.defined;
say 'Verbosity ', ($verbose ?? 'on' !! 'off');
exit 1;
}
sub USAGE
如果对于给定的命令行参数没有找到 MAIN
的多个候选者,则调用 sub USAGE
。 如果没有找到此类方法,则输出生成的使用消息。
sub MAIN(Int $i){ say $i == 42 ?? 'answer' !! 'dunno' }
sub USAGE(){
print Q:c:to/EOH/;
Usage: {$*PROGRAM-NAME} [number]
Prints the answer or 'dunno'.
EOH
}