第十一天-Raku 中所有的星号

All the Stars of Raku, or * ** *

在今年的 Raku Advent Calendar 中, 雪花被今天的博客文章承包了。 我们将检阅使用了 * 字符的结构。 在 Raku 中,根据上下文的不同,您可以叫它星星(或者,如果你愿意的话,可以叫它星号)或者 whatever

Raku 不是一个隐秘的编程语言, 在许多方面它的语法比 Perl 5 更加一致。另一方面,有些地方需要花时间来开启对语法的信心。

让我们看看 * 的不同用法,从最简单的开始,旨在了解最烧脑的例如 * ** *

前两种用法很简单,不需要太多的讨论:

1. 乘法

单个星号用于乘法。严格来讲, 这是一个中缀操作符 infix:<*>, 它的返回值为 Numeric

say 20 * 18; # 360

2. 幂

两个星号 ** 是幂操作符。再次, 这是一个中缀操作符 infix:<**>, 它返回 Numeric 结果, 计算两个给定值点幂。

say pi ** e; # 22.4591577183611

正则表达式中同样也使用了两个标记(***),它们表示不同的东西。 Raku 的一个特点是它可以很容易地在不同的语言之间切换。 正则表达式和 grammar 都是这样的内部语言的例子,其中同样的符号在 Raku 中可能意味着不同的含义。

3. 零或多次重复

* 号量词这个语法条目和 Perl 5 中点行为类似: 允许原子的零次或多次重复。

my $weather = '*****';
my $snow = $weather ~~ / ('*'*) /;
say 'Snow level is ' ~ $snow.chars; # Snow level is 5

当然, 我们还在这儿看到了同一个字符的另一种用法, * 字面量。

4. Min 到 Max 次重复

两个 ** 号是另一个量词的一部分,它指定了最小和最大重复次数:

my $operator = '..';
say "'$operator' is a valid Raku operator"
    if $operator ~~ /^ '.' ** 1..3 $/;

在这个例子中,预计这个点会被重复一次,两次或三次; 不多也不少。

让我们超前一点儿,以 Whatever 符号的角色(剧场中的角色,而不是 Raku 的面向对象编程)使用星号:

my $phrase = 'I love you......';
say 'You are so uncertain...'
    if $phrase ~~ / '.' ** 4..* /;

范围的第二个端点是打开的,这个正则表达式接受所有其中包含四个点以上的短语。

5. 吞噬参数

在子例程签名的数组参数之前的星号意味着吞噬参数 - 将单独的标量参数吞噬进单个数组中。

list-gifts('chocolade', 'ipad', 'camelia', 'raku');

sub list-gifts(*@items) {
    say 'Look at my gifts this year:';
    .say for @items;
}

哈希也允许吞噬参数:

dump(alpha => 'a', beta => 'b'); # Prints:
                                 # alpha = a
                                 # beta = b

sub dump(*%data) {
    for %data.kv {say "$^a = $^b"}
}

请注意,与 Perl 5 不同的是,如果您省略函数签名中的星号,代码将无法编译,因为 Raku 就是说一不二:

Too few positionals passed; expected 1 argument but got 0

6. 吨吨吨吨吨吨吨

**@ 也能工作,但是当你传递数组或列表的时候请注意其中的区别。

带一颗星星:

my @a = < chocolade ipad >;
my @b = < camelia raku >;

all-together(@a, @b);
all-together(['chocolade', 'ipad'], ['camelia', 'raku']);
all-together(< chocolade ipad >, < camelia raku >);

sub all-together(*@items) {
    .say for @items;
}

目前,无论参数列表传递的方式如何,每个礼物都被单独打印了出来。

chocolade
ipad
camelia
raku
chocolade
ipad
camelia
raku
chocolade
ipad
camelia
raku

带俩颗星星:

keep-groupped(@a, @b);
keep-groupped(['chocolade', 'ipad'], ['camelia', 'raku']);
keep-groupped(< chocolade ipad >, < camelia raku >);

sub keep-groupped(**@items) {
    .say for @items;
}

这一次,@items 数组只有两个元素,反映了参数的结构类型:

[chocolade ipad]
[camelia raku]

(chocolade ipad)
(camelia raku)

7. 动态作用域

* twigil,引入了动态作用域。 动态变量和全局变量很容易搞混淆,所以最好测试下面的代码。

sub happy-new-year() {
    "Happy new $*year year!"
}

my $*year = 2018;
say happy-new-year(); # 输出 Happy new 2018 year!

如果你省略了星号, 那么代码就运行不了:

Variable '$year' is not declared

更正它的唯一方法是将 $year 的定义移到函数定义的上面。 使用动态变量 $*year,函数被调用的地方定义了结果。 $*year 变量在子例程的外部作用域中是不可见的,但是在动态作用域内是可见的。

对于动态变量,将新值赋给现有变量还是创建新变量并不重要:

sub happy-new-year() {
    "Happy new $*year year!"
}

my $*year = 2018;
say happy-new-year();

{
    $*year = 2019;        # New value
    say happy-new-year(); # 2019
}

{
    my $*year = 2020;     # New variable
    say happy-new-year(); # 2020
}

8. 编译变量

Raku 提供了许多伪动态常量, 例如:

say $*PERL;      # Raku (6.c)
say @*ARGS;      # Prints command-line arguments
say %*ENV<HOME>; # Prints home directory

9. All methods

.* postfix 伪操作符调用给定名称的所有方法,名称可以在给定的对象中找到,并返回一个结果列表。 在微不足道的情况下,你会得到一个学术上荒诞不羁的代码:

6.*perl.*say; # (6 Int.new)

带星号的代码与不带星号代码有些不同:

pi.perl.say; # 3.14159265358979e0 (notice the scientific
             # format, unlike pi.say)

.* postfix 的真正威力来自于继承。 它有时有助于揭示真相:

class Present {
    method giver() {
        'parents'
    }
}

class ChristmasPresent is Present {
    method giver() {
        'Santa Claus'
    }
}

my ChristmasPresent $present;

$present.giver.say;             # Santa Claus
$present.*giver.join(', ').say; # Santa Claus, parents

一个星号就差别很大!


现在,到了 Raku 最神秘的部分。接下来的两个概念,WhateverWhateverCode 类,很容易混淆在一起。 让我们试着做对吧。

10. Whatever

单个星号 * 能表示任何东西(Whatever)。 Whatever 在 Raku 中是一个预定义好的类, 它在某些有用的场景下引入了一些规定好的行为。

例如,在范围和序列中,最后的 * 表示无穷大。 我们今天已经看到了一个例子。 这是另一个:

.say for 1 .. *;

这个单行程序具有非常高的能量转换效率,因为它产生了一个递增整数的无限列表。 如果你要继续,请按 Ctrl + C

范围 1 .. *1 .. Inf 相同。 您可以清楚地看到,如果您跳转到 Rakudo Raku 源文件并在 src/core/Range.pm 文件的 Range 类的实现中找到如下定义:

multi method new(Whatever \min,Whatever \max,:$excludes-min,:$excludes-max){
    nqp::create(self)!SET-SELF(-Inf,Inf,$excludes-min,$excludes-max,1);
}
multi method new(Whatever \min, \max, :$excludes-min, :$excludes-max) {
    nqp::create(self)!SET-SELF(-Inf,max,$excludes-min,$excludes-max,1);
}
multi method new(\min, Whatever \max, :$excludes-min, :$excludes-max) {
    nqp::create(self)!SET-SELF(min,Inf,$excludes-min,$excludes-max,1);
}

这三个 multi 构造函数描述了三种情况:* .. ** .. $n$n .. *,它们被立即转换为 -Inf .. Inf-Inf .. $n$n .. Inf

作为一个圣诞故事,这里有一个小小的插曲,表明 * 不仅仅是一个 Inf。 有两个到 src/core/Whatever.pm 的提交:

首先,2015年9月16日,"MakeWhatever.new == Inf True:"

  my class Whatever {
      multi method ACCEPTS(Whatever:D: $topic) { True }
      multi method perl(Whatever:D:) { '*' }
+     multi method Numeric(Whatever:D:) { Inf }
  }

几周之后, 在2015年10月23日,"* no longer defaults to Inf",这是为了保护其他 dwimmy 情况下的扩展性:

  my class Whatever {
      multi method ACCEPTS(Whatever:D: $topic) { True }
      multi method perl(Whatever:D:) { '*' }
-     multi method Numeric(Whatever:D:) { Inf }
  }

回到我们更实际的问题,让我们创建自己的使用 whatever 符号 * 的类,。 下面是一个简单的例子,它带有一个接收 Int 值或者 Whatever 的 multi-方法。

class N {
    multi method display(Int $n) {
        say $n;
    }

    multi method display(Whatever) {
        say 2000 + 100.rand.Int;
    }
}

在第一种情况下,该方法只是打印该值。 第二种方法是打印一个在 2000 到 2100 之间的随机数。 因为第二种方法的唯一参数是 Whatever,所以签名中不需要变量。

下面是你如何使用这个类:

my $n = N.new;
$n.display(2018);
$n.display(*);

第一个调用回显它的参数,而第二个调用打印某些随机的东西。

Whatever 符号可以作为一个裸的 Whatever。 假如,你创建一个 echo 函数,并将 * 传递给它:

sub echo($x) {
    say $x;
}

echo(2018); # 2018
echo(*);    # *

这一次,没有魔术发生,该程序打印一个星号。

现在我们正处在一个四两拨千斤的节骨眼上。

11. WhateverCode

最后, 我们来谈谈 WhateverCode

取一个数组然后打印出它的最后一个元素。如果你使用 Perl 5 的风格来做, 你会键入 @a[-1] 那样的东西。在 Raku 中, 那会产生错误:

Unsupported use of a negative -1 subscript
to index from the end; in Raku please
use a function such as *-1

编译器建议使用一个函数, 例如 *-1。它是函数吗?是的, 更准确的说, 它是一个 WhateverCode 块:

say (*-1).WHAT; # (WhateverCode)

现在, 打印数组的后半部分:

my @a = < one two three four five six >;
say @a[3..*]; # (four five six)

数组的索引的范围是 3 .. *Whatever 作为 range 的右端意味着从数组中取出所有剩余的元素。 3 .. * 的类型是 Range:

say (3..*).WHAT; # (Range)

最后,减少一个元素。 我们已经看到,要指定最后一个元素,必须要使用诸如 *-1 的函数。 在 range 的右端可以做同样的事情:

say @a[3 .. *-2]; # (four five)

在这个时候,发生了所谓的 Whatever-柯里化Range 变成了 WhateverCode:

say (3 .. *-2).WHAT; # (WhateverCode)

WhateverCode 是一个内置的 Raku 类名称; 它可以很容易地用于方法分派。 让我们更新上一节中的代码,并添加一个方法变体,它需要一个 WhateverCode 参数:

class N {
    multi method display(Int $n) {
        say $n;
    }

    multi method display(Whatever) {
        say 2000 + 100.rand.Int;
    }

    multi method display(WhateverCode $code) {
        say $code(2000 + 100.rand.Int);
    }
}

现在,参数列表中的星号要么落入 display(Whatever), 要么落入 display(WhateverCode):

N.display(2018);     # display(Int $n)

N.display(*);        # display(Whatever)

N.display(* / 2);    # display(WhateverCode $code)
N.display(* - 1000); # display(WhateverCode $code)

我们再来看看 display 方法中的签名:

multi method display(WhateverCode $code)

$code 参数被用作方法内的函数引用:

say $code(2000 + 100.rand.Int);

该函数需要一个参数,但它会去哪里? 或者换句话说,函数体是什么,在哪里? 我们将该方法调用为 N.display(* / 2)N.display(* - 1000)。 答案是 * / 2* - 1000 都是函数! 还记得编译器关于使用诸如 *-1 之类的函数的提示吗?

这里的星号成为第一个函数参数,因此 * / 2 相当于 {$^a / 2},而 *-1000 相当于 {$^a - 1000}

这是否意味着可以在 $^a 的旁边使用 $^b? 当然! 使 WhateverCode 块接受两个参数。 你如何指出其中的第二个? 毫不惊喜,再用一个星号! 让我们将 display 方法的第四个变体添加到我们的类中:

multi method display(WhateverCode $code 
                     where {$code.arity == 2}) {
    say $code(2000, 100.rand.Int);
}

这里,使用 where 块来缩小调度范围,只选择那些有两个参数的 WhateverCode 块。 完成此操作后,方法调用中将允许含有两个雪花:

N.display( * + * );
N.display( * - * );

这些调用定义了用于计算结果的函数 $code。 所以,N.display(* + *) 背后的实际操作如下:2000 + 100.rand.Int

需要更多的雪花吗? 多添加点星星:

N.display( * * * );
N.display( * ** * );

类似地, 里面实际的计算是:

2000 * 100.rand.Int

2000 ** 100.rand.Int

恭喜! 你现在可以像编译器那样毫不费力地解析 * ** * 结构了。

作业

到目前为止,Raku 给了我们很多圣诞礼物。 让我们回过头来做一下练习并回答一下问题:下面代码中的每个星号在意味着什么?

my @n = 
    ((0, 1, * + * ... *).grep: *.is-prime).map: * * * * *;
.say for @n[^5];

D’哦。 我建议我们从转换代码开始来摆脱所有的星号,并使用不同的语法。

序列运算符 ... 之后的 * 意味着无限地生成序列,所以用 Inf 来代替它:

((0, 1, * + * ... Inf).grep: *.is-prime).map: * * * * *

生成器函数中的两个星号 * + * 可以用一个带有两个显式参数的 lambda 函数来替换:

((0, 1, -> $x, $y {$x + $y} ... Inf).grep: 
    *.is-prime).map: * * * * *

现在,简单的语法交替。 用带圆括号的方法调用替换 .grep。 它的参数 *.is-prime 变成一个代码块,并且星号被替换为默认变量 $_。 请注意,代码使用 * 时不需要花括号。

(0, 1, -> $x, $y {$x + $y} ... Inf).grep({
    $_.is-prime
}).map: * * * * *

最后,与 .map 相同的技巧:但是这次这个方法有三个参数,因此,你可以编写 {$^a * $^b * $^c} 而不是 * * * * *,这里是新的 完整程序的变体:

my @n = (0, 1, -> $x, $y {$x + $y} ... Inf).grep({
        $_.is-prime
    }).map({
        $^a * $^b * $^c
    });
.say for @n[^5];

现在很明显,代码打印了三个斐波那契素数组积的前五个。

附加题

在教科书中,最具挑战性的任务是用 * 标记的。 这里有几个由你自己来解决。

    1. Raku 中的 chdir('/')&*chdir('/') 有什么区别?
    1. 解释下面的 Raku 代码并修改它以展示其优点:.say for 1 ... **

❄❄❄

今天就这样了。 我希望你喜欢 Raku 的强大功能和表现力。今天,我们只谈到了一个 ASCII 字符。 想象一下,如果考虑到该语言在当今编程语言中提供了最好的 Unicode 支持,Raku 的 Universe 是多么的庞大。

今天享受 Raku,并传播这个词! 请继续关注 Raku Advent Calendar; 更多的文章正在等待你的关注,明天就要来了。

Andrew Shitov

Day 11 — All the Stars of Raku, or * ** *

comments powered by Disqus