Raku 中的操作符(二)

  • infix:<…>, 序列操作符.

作为一个中缀操作符, ... 操作符的左右两侧都有一个列表,并且为了产生想要的值的序列,序列操作符 ... 会尽可能地对序列进行惰性求值。列表被展平后求值。就像所有的中缀操作符一样, … 序列操作符比逗号的优先级要低,所以你没必要在逗号列表的两侧加上圆括号。

序列操作符 ... 以右侧列表的第一个值开始。这只在右侧的列表中, … 序列操作符唯一感兴趣的值;any additional list elements are treasured up lazily to be returned after the … is done.

… 右侧的第一个值是序列的端点或界限,这是序列操作符 … 从左侧生成的。

一旦我们知道了序列的边界,左侧的列表会一项一项地被求值,并且普通数字或字符串值被无差异地传递(在右侧边界允许的程度内)如果序列中的任何值匹配到边界值,序列会终止,包括那个最后的边界值在内。要排除边界值,使用 …^ 代替。

在内部,这两种形式用于检测匿名的循环是否会终止,而循环返回序列中的值。假设下一个候选的值存储在 $x 中,并且右侧序列中的第一个值存储在 $limit 中,这两个操作符各自实现为:

    ...     last($x) if $x ~~ $limit;
    ...^    last     if $x ~~ $limit;

如果边界是 * ,序列就没有界限。如果边界是一个闭包,它会在当前候选对象中进行布尔真值求值,并且序列会一直继续只要闭包返回false。如果边界是一个含有一个或无限参数的闭包,

    my $lim = 0;
    1,2,3 ...^ * > $lim      # returns (), since 1 > 0

这个操作符如果只能把左边的值原样返回就太乏味了。它的强大来自于能从旧值生成新值。你可以,例如,使用一个存在的生成器产生一个无穷列表:

    1..* ... * >= $lim
    @fib ... * >= $lim

例如:

> 1..* ... * >= 10  # 1 2 3 4 5 6 7 8 9 10

更一般地,如果 … 操作符左侧列表中的下一项是一个闭包,它不会被返回。他会在已经存在的列表的末尾被调用以产生一个新值。闭包中变量的数目决定了要使用多少前置值作为输入来生成序列中的下一个值。例如,以2为步长计数只需要一个参数:

    2, { $^a + 2 } ... *           # 2,4,6,8,10,12,14,16...

生成裴波纳契序列一次需要两个参数:

    1, 1, { $^a + $^b } ... *      # 1,1,2,3,5,8,13,21...

任何特定的函数也有效,只要你把它作为列表中的一个值而不是调用它:

    1, 1, &infix:<+> ... *         # 1,1,2,3,5,8...
    1, 1, &[+] ... *               # 同上
    1, 1, *+* ... *                # 同上
> sub infix:<jia>($a,$b){ return $a+$b }
> 1 jia 5 # 6
> 1,1,&[jia] ... * # 1 1 2 3 5 8 13 21 34 55 89 144 233 377 ......

更一般地,函数是一元的,这种情况下左侧列表中任何额外的值会被构建为人类可读的文档:

    0,2,4, { $_ + 2 } ... 42        #  0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42

使用闭包:

> 0,2,4,-> $init {$init+2} ... 42   # 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42
    0,2,4, *+2 ... 42           # same thing
    <a b c>, { .succ } ... *    # same as 'a'..*

函数也可以不是单调函数:

    1, -* ... *                # 1, -1, 1, -1, 1, -1...
    False, &prefix:<!> ... *   # False, True, False...

函数也可以是 0-ary 的, 这个时候让闭包成为第一个元素是 okay 的:

{ rand } ... *             # 一组随机数

函数还可以是吞噬的(n-ary),在这种情况下,所有前面的值都被传递进来(这意味着它们必须都被操作符缓存下来,所以性能会经受考验, 你可能会发现自己空间泄露了)

函数的数量不必与返回值的数量匹配, 但是如果确实匹配, 你可能会插入不相关的序列:

    1,1,{ $^a + 1, $^b * 2 } ... *   # 1,1,2,2,3,4,4,8,5,16,6,32...
    1,1, ->$a,$b {$a+1,$b*2} ... *  # 同上

注意在这种情况下任何界限测试被应用到从函数返回的整个 parcel 上, 这个 parcel 包含两个值。

除了那些函数签名所隐含的约束,序列操作符从一个没有对序列作类型约束的显式函数中生成。如果函数的签名和已存在的值不匹配,那么序列终止。

S03-sequence/misc.t lines 5–12

如果没有提供生成闭包, 并且序列是数字的,而且序列明显还是等差的或等比的(通过检查它的最后 3 个值),那么序列操作符就会推断出合适的函数:

1, 3, 5 ... *    # 奇数
1, 2, 4 ... *    # 2的幂
10,9, 8 ... 0    # 倒数

即, 假设我们调用了最后 3 个数字 $a, $b, 和 $c,然后定义:

$ab = $b - $a;
$bc = $c - $b;

如果 $ab == $bc 并且 $ab 不等于0, 那么我们通过函数 *+$ab 可以推断出等差数列。如果 $ab 为 0,并且那3个值看起来像数字,那么函数推断为 *+0

如果它们看起来不像数字,那么根据 $b cmp $c 的结果是 Increasing 还是 Decreasing 来决定选择的函数是 *.succ 还是 *.pred

如果 cmp 返回 Same 那么会假定函数是同一个。

如果 $ab != $bc 并且 none($a,$b,$c) == 0, 那么会使用除法而非减法做类似的计算来决定是否需要一个等比数列。定义:

$ab = $b / $a;
$bc = $c / $b;

如果两者的商相等(并且是有限的),那么会推断出等比函数 {$_ * $bc}

如果目前为止只有 2 个值, $a 和 $b, 并且差值 $ab 不为 0, 那么我们就假定函数为 *+$ab 的等差数列。

如果 $ab 是 0, 那么再次, 我们使用 *+0 还是 *.succ/*.pred 取决于那两个值看起来是不是数字。

如果只有一个值,我们总是通过 *.succ 假定增量(这可能被强制为 .pred 通过检查界限,就像下面指定的那样)因此这些结果都是一样的:

1 .. *
1 ... *
1,2 ... *
1,2,3 ... *
<1 2 3> ... *

同样地, 如果给定的值(s)不是数字, 就假定为 .succ, 所以这些结果是一样的:

'a' .. *
'a' ... *
'a','b' ... *
'a','b','c' ... *
<a b c> ... *

如果序列操作符的左侧是 (), 那我们使用函数 {()} 来生成一个无限的空序列。

如果给定了界限,那这个界限就必须被精确匹配。如果不能精确匹配,会产生无限列表。例如,因为"趋近"和“相等”不一样, 下面的两个序列都是无限列表,就像你把界限指定为了 * 而不是 0:

1,1/2,1/4 ... 0    # like 1,1/2,1/4 ... *
1,-1/2,1/4 ... 0   # like 1,-1/2,1/4 ... *

同样地,这是所有的偶数:

my $end = 7;
0,2,4 ... $end

为了捕捉这样一种情况, 建议写一个不等式代替:

0,2,4 ...^ { $_ > $end }

当使用了显式的界限函数,他可能通过返回任意真值来终止它的列表。因为序列操作符是列表结合性的,内部函数后面可以跟着 … ,然后另外一个函数来继续列表,等等。因此:

S03-sequence/misc.t lines 34–100

    1,   *+1   ... { $_ ==   9 },
    10,  *+10  ... { $_ ==  90 },
    100, *+100 ... { $_ == 900 }

产生:

    1,2,3,4,5,6,7,8,9,
    10,20,30,40,50,60,70,80,90,
    100,200,300,400,500,600,700,800,900

考虑到没有闭包的普通匹配规则,我们可以把上面的序列更简单地写为:

    1, 2, 3 ... 9,
    10, 20, 30 ... 90,
    100, 200, 300 ... 900

甚至仅仅是:

    1, 2, 3 ...
    10, 20, 30 ...
    100, 200, 300 ... 900

因为一个精确的匹配界限会被作为序列的一部分返回,所以提供的那个精确值是一个合适类型的值, 而非一个闭包。

对于左侧只有一个值的函数推断,最后的值被用于决定是 *.succ 还是 *.pred 更合适。 使用 cmp 来比较那两个值来决定前进的方向。

因此, 序列操作符能自动反转, 而范围操作符不会自动反转.

    'z' .. 'a'   # 表示一个空的范围
    'z' ... 'a'  # z y x ... a

你可以使用 ^... 形式来排除第一个值:

    'z' ^... 'a' # y x ... a
    5 ^... 1     # 4, 3, 2, 1

但是你要意识到, 如果列表的左侧很复杂, 特别是左侧是另一个序列时, 肯定会让你的读者困惑:

    1, 2, 3 ^... *;                  # 2, 3 ...  !
    1, 2, 3 ... 10, 20, 30 ^... *;   # 2, 3 ...  !?!?

是的, 对于极端喜欢对称性的那些人来说, 还有另外一种形式: ^...^

就像数字数值一样, 字符串匹配必须是精确的, 否则会产生无限序列.

注意下面这个序列:

    1.0, *+0.2 ... 2.0

是使用 Rat 算术计算的, 而不是 Num, 所以 2.0 精确地匹配了并结束了序列.

注意:只有在期望为一个 term 的地方,... 才会被识别为 yada 操作符。序列操作符只用于期望中缀操作符出现的地方。

如果你在 ... 前面放置了一个逗号,那么 ...会被识别为 yada 列表操作符 - 表达当列表到达那点时会失败的要求。

1..20, ... "I only know up to 20 so far mister"
> 1..20, fail "I only know up to 20 so far mister"  # I only know up to 20 so far mister

序列的末端是一个代表单个代码点的字符串时,会抛出一个特殊的异常, 因为典型地,用户会把这样一个字符串看作是字符而非字符串。如果你像这样说:

S03-sequence/nonnumeric.t lines 36–101

    'A' ... 'z'
    "\xff" ... "\0"
>  'A' ... 'z' # A B C D ... Y Z [ \ ] ^ _ ` a  b c ... y z

它会假定你对按字母顺序范围不感兴趣, 所以,代替对于字符串使用普通的 .succ/.pred, 它会像这样使用单调函数来增加或减少底层的代码点数字:

'A', { $^prev.ord.succ.chr } ... 'z';
"\xff", { $^prev.ord.pred.chr } ... "\0";

… 操作符是 “yada, yada, yada” 列表操作符, 它在其它东西之间用作函数原型中得主体。如果它曾经被执行过的话会强烈地抱怨(通过调用 fail)。变体 ??? 调用 warn, 而 !!! 调用 die。参数是可选的,但是如果提供了参数的话,会被传递给 failwarndie 。 否则系统会基于上下文为你编造信息,以标示你尝试执行了某些已经创建过的东西。

Reduce 操作符

[+] [*] [<] [\+] [\*] 等等.

Sigils as coercions to roles

Sigil       Alpha variant
-----       -------------
$           Scalar
@           Positional (or Iterable?)
%           Associative
&           Callable
 $(1,2 Z 3,4)      # Scalar((1,3),(2,4))
 @(1,2 Z 3,4)      # ((1,3),(2,4))
 %(1,2 Z 3,4)      # PairSeq(1 => 3, 2 => 4)
 $(1,2 X 3,4)      # Scalar((1,3),(1,4),(2,3),(2,4))
 @(1,2 X 3,4)      # ((1,3),(1,4),(2,3),(2,4))

-> 变成了 . , 就像世界其它地方使用的一样。有一个产生编译时错误的伪后缀 postfix:['->'] 操作符以提醒 Perl 5 用户使用点代替。(“pointy block” 在 Raku 中使用 -> 要求它前面有空格,当箭头可能会和后缀混淆时,即,当期望一个中缀时。在 item 位置上不要求有前置空格。)

字符串连接由 . 变成 ~。字符串追加同样变成 ~=

文件测试操作符不见了。我们现在使用 Pair 作为调用对象的方法的模式;

if $filename.IO ~~ :e { say "exists" }

等价于

if so $filename.IO.e { say "exists" }

Likewise

if $filename.IO ~~ :!e { say "doesn't exist" }

is the same as

if not $filename.IO.e { say "doesn't exist" }

如果你不关心右侧某个位置上是什么元素,你可以把这个元素赋值给 * 记号。 最后的星号 * 丢弃掉列表的剩余部分:

($a, *, $c) = 1, 2, 3;      # 丢弃数字 2
($a, $b, $c, *) = 1..42;    # 丢弃 4..42

(在签字语法中,一个裸的 $ 也能忽略单个参数,而一个裸的 *@ 可以忽略剩下的参数。)

列表赋值依次把右侧的列表提供给左侧的每个容器,而每个容器可以从右侧列表的前头接收一个或多个元素。如果左侧的元素有任何剩余,警告就会出现,除非左侧的列表以 * 字符结尾或者右侧的最后的迭代器是按照 * 定义的。因此下面没有一个会发出警告:

($a, $b, $c, *) = 1..9999999;
($a, $b, $c) = 1..*;
($a, $b, $c) = 1 xx *;
($a, $b, $c) = 1, 2, *;

然而,这个会警告你信息缺失:

($a, $b, $c) = 1, 2, 3, 4;

把列表赋值给标量, 你可以这样写:

    $a = 1, 2, 3;
    ($a = 1), 2, 3; # 2 和 3 的上下文为空

显式的禁用或破坏 item 赋值解释:

    $a = [1, 2, 3];             # 强制构建 (或许是最佳实践)
    $a = (1, 2, 3);             # force grouping as syntactic item
    $a = list 1, 2, 3;          # force grouping using listop precedence
    $a = @(1, 2, 3);            # same thing
    @$a = 1, 2, 3;              # 强制列表赋值
    $a[] = 1, 2, 3;             # same thing

如果一个函数是上下文不敏感的并且你想返回一个标量值,如果你想为了下标或右边强制为 item 上下文,那么你必须使用 item(或 $+~)。

    @a[foo()] = bar();           # foo() 和 bar() 在列表上下文中调用
    @a[item foo()] = item bar(); # foo() 和 bar() 在标量上下文中调用
    @a[$(foo())] = $(bar());     # 同样的东西
    @a[+foo()] = +bar();         # foo() 和 bar() 在数值上下文中调用
    %a{~foo()} = ~bar();         # foo() 和 bar() 在字符串上下文中调用

Junctive 操作符

S03-junctions/misc.t lines 23–151 S03-junctions/misc.t lines 152–157 S03-junctions/associative.t lines 16–37 S03-junctions/boolean-context.t lines 5–150

|, &, 和 ^ 不再是位操作符了(查看 “Changes to Perl 5 operators”),而是担任更高的职务: 它们现在是 junction 构造器了。

junction 是等价于多个值的单个值。它们进行线程化操作符, 返回另一个代表结果的 junction:

S03-operators/misc.t lines 68–89 S03-junctions/misc.t lines 158–168 S03-junctions/misc.t lines 233–499

 (1|2|3) + 4;                            # 5|6|7
 (1|2) + (3&4);                          # (4|5) & (5|6)

就像最后那个例子中解释的那样,当单个操作符应用到两个 junctions 中时,结果是一个代表左右可能值得组合的 junction。

Junctions 还有 anyallonenone 的函数变体。

这为像这样的构造打开了大门。

S03-junctions/misc.t lines 169–191

 if $roll == none(1..6) { print "Invalid roll" }
 if $roll == 1|2|3      { print "Low roll"     }

Junctions 在下标中的表现:

S03-junctions/misc.t lines 192–207

doit() if @foo[any(1,2,3)]

Junctions 也是没有顺序的。 所以如果你这样写:

S03-junctions/misc.t lines 208–232

foo() | bar() | baz() == 42

这向编译器表明 junctional 参数之间没有连接(coupling)。它们可以以任意顺序求值或并行地求值。它们也可以是短路的, 只要它们中的任何一个返回 42, 并且其它的不会被运行。或者, 如果并行地运行, 第一个成功的线程会突然终止其它的线程。一般地, 在 junctions 中, 你可能需要避免带有副作用的代码。

把否定操作符和 junctions 用在一起可能会有问题如果原生地自动线程化的话。然而,就否定元操作符而言,通过定义 !=ne,我们自动地得到了说英语的人所想要的 “not raising”。即:

S03-junctions/autothreading.t lines 290–367

if $a != 1 | 2 | 3 {...}

实际上意味着:

if $a ![==] 1 | 2 | 3 {...}

其中元操作符被重写为某些像高阶函数这样的东西:

negate((* == *), $a, (1|2|3));

它最终等价于:

if not $a == 1 | 2 | 3 {...}

它是说英语的人所期望的语义,你自己写出后面那种风格的形式可能会更好。

作用在数组、列表和集合中得 Junctive 方法就像对应的列表操作符那样工作。然而,作用在散列身上的 junctive 方法,只会对散列的键进行连结(junction)。使用 listop 形式(或者一个显式的 .pairs) 来连结(junction) pairs。

各种用于集合和 bags (交集、并集等)的操作符也拥有 junctive 优先级(除了那些返回布尔值得之外,它们实际上被归类为链式操作符)。

comments powered by Disqus