要作为子程序调用还是作为方法调用

楽土

要作为子程序调用还是作为方法调用?这就是问题所在

在 Raku 中,具有自己的词法范围和返回处理的代码对象的总称是例程。虽然不是所有的例程都是子程序和/或方法,但所有的子程序和/或方法都是例程。一个例程既可以是子程序,也可以是方法,在这种情况下,你可以分别使用子程序调用语法和方法调用语法来调用它。例如

is-prime(5); # regular subroutine call syntax
5.is-prime;  # regular method call syntax

尽管如此,即使一个例程只能作为子程序(或方法)使用,你仍然可以把它作为一个方法(或子程序)使用。所需要的只是额外的一点语法。

从子程序到方法

给定一个子程序 f(arg1, arg2, arg3),你可以用元运算符 .& 对它的调用者进行调用,就像 arg1.&f(arg2, arg3) 一样。在这里,调用者将被绑定到第一个位置参数,即 arg1,其余的参数被传递给子程序。例如

'/home'.&dir; # dir '/home';

请注意,'/home'.dir 本来会以 No such method 'dir' for invocant of type 'Str' 的错误而失败,但使用 .& 允许我们使用与常规方法调用语法非常相似的语法。

这对用户定义的子程序也是一样的。例如

sub multiply {
    [*] @_
}

2.&multiply;       # multiply 2;
2.&multiply(3);    # multiply 2, 3;
2.&multiply(3, 4); # multiply 2, 3, 4;

词汇作用域的方法

在 Raku 中,有一个 index 例程可以搜索一个子串,并返回它在目标字符串上的位置。

say "Camelia".index("a");     # 1 
say "Camelia".index("a", 2);  # 6

要从一个字符串中获取多个索引,必须使用 indices

say "Camelia".indices("a"); # (1 6)

然而,你可能会发现,无论是因为你足够谨慎,阅读了要避免的陷阱页面,还是因为你仔细阅读了文档,从不假设事情,还是因为你沿着试错的历程,在一个列表上调用 index 会将其胁迫成一个字符串。这意味着结果可能不是你所期望的。

my @a = <a b c d a>;
say @a.index(‘a’);    # 0 
say @a.index('c');    # 4 -- not 2! 
say @a.index('b c');  # 2 -- not undefined! 
say @a.index(<a b>);  # 0 -- not undefined!

注意:这里实现的 indices 方法只是为了说明问题。我们鼓励你使用 grep 来实现同样的目的(甚至更多)。

知道了这一点,我们可能会受到启发,决定为列表创建一个 indices 例程。它可能看起来像下面这样。

my method indices( @items: &criterion where *.arity ≤ 1, :$all = False ) {
    my Int @idxs;
    for @items.kv -> $index, $value {
       return $index      if criterion($value) && !$all;
       @idxs.push($index) if criterion($value);
    }
    return @idxs if @idxs;
    return Nil;
}

在这里,我们在类定义之外声明了一个方法,因此并没有附加到任何类上,我们决定在方法的签名中显式地声明调用者(@items),但不需要这样。我们决定在方法的签名中显式地声明调用者(@items),但不需要这样。例如,我们可以不使用 @items,而使用 self(例如,in for self.kv),即一个方法默认的调用者(方法中的显式调用者将在下面讨论)。

默认情况下,这个版本的 indices 会返回第一个索引,从而模拟 Strindex 行为。传递标志 :all 使它的行为与 Strindices 相似。因此,下面的工作就像预期的那样。

my @a = <a b c d a>;
say @a.&indices({$_ eq 'a'});       # 0
say @a.&indices({$_ eq 'a'}, :all); # [0 4]  
say @a.&indices({$_ eq 'c'});       # 2 
say @a.&indices({$_ eq 'b c'});     # Nil
say @a.&indices({$_ eq <a b>});     # Nil

的确,我们仍然必须使用 .& methodop 才能调用它,这一点是可以理解的,因为我们面对的是一个没有附加到任何特定类的方法。这超出了本文的初衷,但事实上,我们可以通过使用 augment 声明器将前面的方法直接添加到 Array 类中(你必须先启用 MONKEY-TYPING pragma)。例如

use MONKEY-TYPING;

augment class Array {
    method indices( @items: &criterion where *.arity ≤ 1, :$all = False ) {
        my Int @idxs;
        for @items.kv -> $index, $value {
           return $index      if criterion($value) && !$all;
           @idxs.push($index) if criterion($value);
        }
        return @idxs if @idxs;
        return Nil;
    }
}

因此,现在方法 indicesArray 类的一部分,可以像普通方法一样对 Array 变量进行调用。

my @a = <a b c d>;
say @a.indices({$_ eq 'a'});        # 0
say @a.indices({$_ eq 'a'}, :all);  # [0 4] 
say @a.indices({$_ eq 'c'});        # 2 
say @a.indices({$_ eq 'b c'});      # Nil
say @a.indices({$_ eq <a b>});      # Nil

从方法到子程序

我们已经知道如何在对象上调用方法(即 obj.method(args))。要将一个方法作为子程序调用,我们使用 method(obj: args) 语法(注意 invocant 后面的冒号)。这里对象成为方法的显式调用者。例如:

base(255: 16); #=> 255.base(16); 
base 255: 16;  # same

绕道而行

method(obj: args) 的语法让人联想到,在 Raku 中,你可以给方法中的调用者起一些不同的名字,而不是仅仅使用 self,也就是默认情况下作为每个方法主体的第一个位置参数隐式传递的东西。例如,让我们考虑以下类。

class Point2D {
    has  $.x;
    has  $.y;
}

找出两个 Point2D 点之间距离的方法可以实现如下。

method distance( Point2D $B ) {
    sqrt ($!x - $B.x) ** 2 + ($!y - $B.y) ** 2
}

在这里,我们是直接访问调用者的属性($!x$!y),而通过它的访问器访问所提供的参数的属性(例如,$B.x)。然而,如果我们要依赖这两个点的访问器方法,我们也可以这样声明距离方法。

method distance( Point2D $B ) {
    # self is available inside any method and bound to the invocant.
    sqrt (self.x - $B.x) ** 2 + (self.y - $B.y) ** 2
}

尽管如此,我们还是希望向阅读我们代码的人更明确地表达我们的意图(例如,找到从A点到B点的距离)。我们可以通过提供一个显式的调用者来实现这一目的,具体做法是将调用者指定为一个位置参数,后面跟着一个冒号(:),然后再跟着方法的常用参数(如果有的话)。

# get distance from point $A to point $B.
method distance( $A: Point2D $B ) {
    sqrt ($A.x - $B.x) ** 2 + ($A.y - $B.y) ** 2
}

当然,无论是使用 self 还是显式 invocant 都没有太大的区别,但后一种选择可能会使方法更加清晰。

我们可以更进一步,限制方法应该被调用的 invocant 的类型。因此,该方法可以被调用在一个类型对象上(在这种情况下,我们有一个类方法),或者调用在一个对象实例上(在这种情况下,我们有一个对象方法)。为此,我们必须使用 ::?CLASS 变量,放在冒号前面,它与 :U(对于类方法)或 :D(对于对象方法)相结合。值得指出的是,这对隐式和显式调用者都有效,如下图所示。

method distance( ::?CLASS:D $A: Point $B ) {
    sqrt ($A.x - $B.x) ** 2 + ($A.y - $B.y) ** 2
}

method dimensions( ::?CLASS:U : ) {
    2
}

注意:在 ::?CLASS、显式调用者和 : 周围的空白并不是严格必要的,但它有助于方法签名的可读性。

因此,我们有:

my Point2D $x .= new(:x(4), :y(6));
my Point2D $y .= new(:x(7), :y(10));

say $x.distance($y);      #=> 5
say Point2D.distance($y); # Error: X::Parameter::InvalidConcretenessInvocant

say $x.dimensions;      # Error: X::Parameter::InvalidConcretenessInvocant
say Point2D.dimensions; #=> 2

最后,在 Raku 中,将一个方法声明为类方法或对象方法并不是非此即彼的情况。如果你想让一个方法既是类方法又是对象方法,只需使用 multi 声明器即可。

括号

除非为了消除嵌套调用的歧义,子程序调用中的括号可以省略。

#| Return a list of its arguments squared.
sub list-of-squares {
    slip @_.map(* ** 2)
}

say sum 1, list-of-squares 2, 3, 4, 5;  # 55 = 1 + 4 + 9 + 16 + 25
say sum 1, list-of-squares(2, 3), 4, 5; # 23 = 1 + 4 + 9 + 4 + 5

对于方法调用的语法,可以用冒号来代替括号。例如:

say 255.base(1 + 3 ** 2 + 6); 
say 255.base: 1 + 3 ** 2 + 6;

在此上下文中,冒号被用作优先级下降,通常会使代码更干净、更容易阅读,特别是处理子程序或块作为参数的方法,如 mapgrepsort 等。例如:

my @scores = 
    %(:name<Barney>, :score<195>),
    %(:name<Fred>,   :score<205>),
    %(:name<Dino>,   :score<30> ),
    %(:name<Barto>,  :score<195>),
    %(:name<Ferla>,  :score<200>),
    %(:name<Telios>, :score<195>),
;  

my @winners = @scores.sort: {
	$^b<score> <=> $^a<score>  # descending numeric score
	or
	$^a<name> cmp $^b<name>    # break ties by name
}; # <= This semicolon isn't necessary here ¯\_(ツ)_/¯

for @winners {
    printf "%-10s %d\n", $_<name>, $_<score>;
}

值得一提的是,这里使用的冒号是方法调用语法的替代,而不是子程序调用语法的替代。因此,在:

log: 32, 2; #=> 5

冒号是在不同的上下文中使用的;它创建了一个标签 log,而不是调用带有参数的函数 log。结果,我们得到了警告: Useless use of ... in sink context。然而,log 例程也被定义为方法,所以我们仍然可以使用冒号作为优先级下降。

# as a method
32.log: 2; #=> 5

# and as a function call
log 32, 2; #=> 5

在一个被用作方法的用户定义子程序中,我们可以使用冒号作为优先级下降符吗?答案是可以的。前面的例子中的 multiply 子程序可以改写为:

2.&multiply;       # multiply 2;
2.&multiply: 3;    # multiply 2, 3;
2.&multiply: 3, 4; # multiply 2, 3, 4;

在 Raku 中,很多地方都有一个功能/结构,并很好地将不同但相关的应用联系在一起。这就是其中的一个结构。然而,Raku 对冒号的喜爱并不止步于此,冒号渗透到了整个语言中。如果你对冒号的不同用途感兴趣,下面的文章可能会有所帮助。

总结

  • 使用 .& 来调用一个子程序,就像 arg1.&sub(arg2, arg3, ...) 中的方法一样。

  • 使用冒号 : 来调用一个方法作为子程序,如 method(obj: arg1, arg2, ...)

  • 在子程序调用中可以省略括号,除非是为了消除嵌套调用的歧义。

  • 在方法调用语法中,括号可以用冒号代替。这在向方法传递子程序或块时特别有用。

comments powered by Disqus