Desgin Perl 6: S07-Lists

pushappend 的表现不同, push 一次只添加单个参数到列表末端, append 一次可以添加多个参数。

use v6;

my @d = ( [ 1 .. 3 ] );
@d.push( [ 4 .. 6 ] );
@d.push( [ 7 .. 9 ] );


for @d -> $r {
    say "$r[]";
}
# 1
# 2
# 3
# 4 5 6
# 7 8 9

for @d -> $r { say $r.WHAT() }
# (Int)
# (Int)
# (Int)
# (Array) 整个数组作为单个参数
# (Array)

say @d.perl;
# [1, 2, 3, [4, 5, 6], [7, 8, 9]]

使用 append 一次能追加多个元素:

use v6;

my @d =  ( [ 1 .. 3 ] );
@d.append( [ 4 .. 6 ] );
@d.append( [ 7 .. 9 ] );

for @d -> $item {
    say "$item[]";
}
# 打印 1\n2\n3\n4\n5\n6\n7\n8\n9
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

这跟The single argument rule有关。

设计大纲


Perl 6 提供了很多跟列表相关的特性, 包括 eager, lazy, 并行计算, 还有紧凑型阵列和多维数组存储。

Sequences vs. Lists


在 Perl 6 中, 我们使用项 sequence 来来指某个东西, 当需要的时候产生一个值的序列。即序列是惰性的。注意, 只能要求生成一次。我们使用项list来指能保存值的东西。

(1, 2, 3)    # 列表, 最简单的 list
[1, 2, 3]    # 数组, Scalar 容器的列表
|(1, 2)      # a Slip, 一个展开到周围列表中的列表
$*IN.lines   # a Seq, 一个可以被连续处理的序列
(^1000).race # a HyperSeq, 一个可以并行处理的序列

The single argument rule


在 Perl 中 @ 符号标示着 "这些"(these), 而 $符号标示着 "这个"(the)。这种复数/单数的明显差别出现在语言中的各种地方, Perl 中的很多便捷就是来源于这个特点。展平(Flattening)就是 @-like 的东西会在特定上下文中自动把它的值并入周围的列表中。之前这在 Perl 中既功能强大又很有迷惑性。在直截了当的发起知名的"单个参数规则"之前, Perl 6 在发展中通过了几次跟 flattening 有关的模仿。

对单个参数规则最好的理解是通过 for循环迭代的次数。对于 for循环, 要迭代的东西总是被当作单个参数。因此有了单个参数规则这个名字。

for 1, 2, 3   { }   # 含有 3 个元素的列表, 3 次迭代
for (1, 2, 3) { }   # 含有 3 个元素的列表, 3 次迭代
for [1, 2, 3] { }   # 含有 3 个元素的数组(存放在 Scalar 中),  3次迭代
for @a, @b    { }   # 含有 2 个元素的列表, 2 次迭代
for (@a,)     { }   # 含有 1 个元素的列表, 1 次迭代
for (@a)      { }   # 含有 @a.elems 个元素的列表, @a.elems 次迭代
for @a        { }   # 含有 @a.elems 个元素的列表, @a.elems 次迭代

前两个是相同的, 因为圆括号事实上不构建列表, 而只是分组。是中缀操作符 infix:<,>组成的列表。第三个也执行了 3 次迭代, 因为在 Perl 6 中 [...] 构建了一个数组但是没有把它包裹进 Scalar 容器中。第四个会执行 2 次迭代, 因为参数是一个含有两个元素的列表, 而那两个元素恰好是数组, 它俩都有 @ 符号, 但是都没有导致展开。第 五 个同样, infix:<,> 很高兴地组成了只含一个元素的列表。

单个参数规则也考虑了 Scalar 容器。因此:

for $(1, 2, 3) { }  # Scalar 容器中的一个列表, 1 次迭代  
for $[1, 2, 3] { }  # Scalar 容器中的一个数组, 1 次迭代  
for $@a        { }  # Scalar 容器中的一个数组, 1 次迭代  
> for $(1, 2, 3) -> $i { say $i.elems }
3
> for $(1, 2, 3) -> $i { say $i }
(1 2 3)
> for $(1, 2, 3) -> $i { say $i.WHAT }
(List)
> for $(1, 2, 3) -> $i { say $i.perl }
$(1, 2, 3)

> for $[1, 2, 3] -> $i { say $i }
[1 2 3]
> for $[1, 2, 3] -> $i { say $i.perl }
$[1, 2, 3]
> for $[1, 2, 3] -> $i { say $i.WHAT }
(Array)
> for $[1, 2, 3] -> $i { say $i.elems }
3

> my  @a = 1,2,3
[1 2 3]
> for $@a -> $a  { say $a.perl }
$[1, 2, 3]

贯穿 Perl 6 语言, 单个参数规则(Single argument rule) 始终如一地被实现了。例如, 我们看 push 方法:

@a.push: 1, 2, 3;       # pushes 3 values to @a
@a.push: [1, 2, 3];     # pushes 1 Array to @a
@a.push: @b;            # pushes 1 Array to @a
@a.push: @b,;           # same, trailing comma doesn't make > 1 argument
@a.push: $(1, 2, 3);    # pushes 1 value (a List) to @a
@a.push: $[1, 2, 3];    # pushes 1 value (an Array) to @a

此外, 列表构造器(例如 infix:<,> 操作符) 和数组构造器([…]环缀)也遵守这个规则:

    [1, 2, 3]               # Array of 3 elements
    [@a, @b]                # Array of 2 elements
    [@a, 1..10]             # Array of 2 elements
    [@a]                    # Array with the elements of @a copied into it
    [1..10]                 # Array with 10 elements
    [$@a]                   # Array with 1 element (@a)
    [@a,]                   # Array with 1 element (@a)
    [[1]]                   # Same as [1]
    [[1],]                  # Array with a single element that is [1]
    [$[1]]                  # Array with a single element that is [1]

所以, 要让最开始的那个例子工作, 使用:

my @d = ( [ 1 .. 3 ], );  # [[1 2 3]]
@d.push: [ 4 .. 6 ];
@d.push: [ 7 .. 9 ];
# [[1 2 3] [4 5 6] [7 8 9]]

或者

my @d = ( $[ 1 .. 3 ]);
@d.push: [ 4 ..6 ];
@d.push: [ 7 ..9 ];

User-level Types


List


List 是不可变的, 可能是无限的, 值的列表。组成 List 最简单的一种方法是使用 infix:<,> 操作符:

1, 2, 3

List 可以被索引, 并且, 假设它是有限的, 也能询问列表中元素的个数:

say (1, 2, 3)[1];    # 2
say (1, 2, 3).elems; # 3

因为List是不可变的, 对它进行 push、pop、shift、unshift 或 splice 是不可能的。 reverserotate 操作会返回新的 Lists

虽然List自身是不可变的, 但是它包含的元素可以是可变的, 包括 Scalar 容器:

my $a = 2;
my $b = 4;

($a, $b)[0]++;
($a, $b)[1] *= 2;
say $a; # 3
say $b; # 8

List 中尝试给不可变值赋值会导致错误:

(1, 2, 3)[0]++; # Dies: 不能给不可变值赋值

Slip


Slip 类型是 List 的一个子类。Slip 会把它的值并入周围的 List 中。

(1, (2, 3), 4).elems      # 3
(1, slip(2, 3), 4).elems  # 4

List 强转为 Slip 是可能的, 所以上面的也能写为:

(1, (2, 3).Slip, 4).elems # 4

在不发生 flattening 的地方使用 Slip 是一种常见的获取 flattening 的方式:

my @a = 1, 2, 3;
my @b = 4, 5;

.say for @a.Slip, @b.Slip;  # 5 次迭代

这有点啰嗦, 使用 prefix:<|>来做 Slip 强转:

my @a = 1, 2, 3;
my @b = 4, 5;

.say for |@a, |@b; # 5 次迭代

|在如下形式中也很有用:

my @prefixed-values = 0, |@values;

这儿, 单个参数规则会使 @prefixed-values 拥有两个元素, 即 0 和 @values。

Slip 类型也可以用在 mapgather/take、和 lazy循环中。下面是一种 map能把多个值放进它的结果流里面的方法:

my @a = 1, 2;
say @a.map({ $_ xx 2 }).elems;      # 2
say @a.map({ |($_ xx 2) }).elems;   # 4

因为 $_ xx 2 产生一个含有两个元素的列表(List)。

Array


ArrayList 的一个子类, 把赋值给数组的值放进 Scalar 容器中, 这意味着数组中的值可以被改变。Array 是 @-sigil 变量得到的默认类型。

my @a = 1, 2, 3;
say @a.WHAT;     # (Array)
@a[1]++;         # Scalar 容器中的值可变
say @a;          # 1 3 3

如果没有 shape 属性, 数组会自动增长:

my @a;
@a[5] = 42;
say @a.elems;  # 6

Array支持 pushpopshiftunshiftsplice

给数组赋值默认是迫切的(eager), 并创建一组新的 Scalar 容器:

my @a = 1, 2, 3;
my @b = @a;

@a[1]++;
say @b;  # 1, 2, 3

注意, [...] 数组构造器等价于创建然后再赋值给一个匿名数组。

Seq


Seq 是单次值生产者。大部分列表处理操作返回 Seq

say (1, 2, 3).map(* + 1).^name;  # Seq
say (1, 2 Z 'a', 'b').^name;     # Seq
say (1, 1, * + * ... *).^name;   # Seq
say $*IN.lines.^name;            # Seq

因为 Seq 默认不会记住它的值(values), 所以 Seq 只能被使用一次。例如, 如果存储了一个 Seq:

my \seq = (1, 2, 3).map(* + 1);

只有第一次迭代会有效, 之后再尝试迭代就会死, 因为值已经被用完了:

for seq { .say }    # 2\n3\n4\n
for seq { .say }    # Dies: This Seq has already been iterated

这意味着你可以确信 for 循环迭代了文件的行:

for open('data').lines {
    .say if /beer/;
}

这不会把文件中的行保持在内存中。此外设立不会把所有行保持在内存中的处理管道也会很容易:

my \lines   = open('products').lines;
my \beer    = lines.grep(/beer/);
my \excited = beer.map(&uc);
.say for excited;

然而, 任何重用 linesbeer、或excited 的尝试都会导致错误。这段程序在性能上等价于:

.say for open('products').lines.grep(/beer/).map(&uc);

但是提供了一个给阶段命名的机会。注意使用 Scalar 变量代替也是可以的, 但是单个参数规则需要最终的循环必须为:

.say for |$excited;

只要序列没有被标记为 lazy, 把 Seq 赋值给数组就会迫切的执行操作并把结果存到数组中。因此, 任何人这样写就不惊讶了:

my @lines   = open('products').lines;
my @beer    = @lines.grep(/beer/);
my @excited = @beer.map(&uc);
.say for @excited;

重用这些数组中的任何一个都没问题。当然, 该程序的内存表现完全不同, 并且它会较慢, 因为它创建了所有的额外的 Scalar 容器(导致额外的垃圾回收)和糟糕的位置引用。(我们不得不在程序的生命周期中多次谈论同一个字符串)。

偶尔, 要求 Seq 缓存自身也有用。这可以通过在Seq 身上调用 cache方法完成, 这从 Seq 得到一个惰性列表并返回它。之后再调用 cache方法会返回同样的惰性列表。注意, 第一次调用 cache方法会被算作消费了Seq, 所以如果之前已经发生了迭代它就不再有效, 而且之后任何在调用完 cache的迭代尝试都会失败。只有 .cache方法能被调用多于1 次。

Seq 不像 List 那样遵守 Positional role。 因此, Seq 不能被绑定给含有 @ 符号的变量:

my @lines := $*IN.lines;  # Dies

这样做的一个后果就是, 原生地, 你不能传递 Seq 作为绑定给@符号的参数:

sub process(@data) {
    
}
process($*IN.lines);

这会极不方便。因此, 签名 binder(它实际使用 ::= 赋值语义而非 :=)会 spot 失败来绑定 @符号参数, 并检查参数是否遵守了 Positional role。 如果遵守了, 那么它会在参数上调用 cache 方法并绑定它的结果代替。

Iterable


SeqList 这俩, 还有 Perl 6 中的各种其它类型, 遵守 Iterable role。这个 role 的主要意图是获得一个 iterator方法。中级 Perl 6 用户很少会关心 iterator方法和它返回什么。

Iterable 的第二个目的是为了标记出会被按需展开的东西, 使用 flat方法或用在它们身上的函数。

my @a = 1, 2, 3;
my @b = 4, 5;

for flat @a, @b { }          # 5 次迭代
say [flat @a, @b].elems;     # 5 次迭代

flat 的另一用途是展开嵌套的列表结构。例如, Z(zip)操作符产生一个列表的列表:

say (1, 2 Z 'a', 'b').perl;  # ((1, "a"), (2, "b")).Seq

flat 能用于展开它们, 这在和使用带有多个参数的尖块 for 循环一块使用时很有用:

for flat 1, 2 Z 'a', 'b' -> $num, $letter  { }

注意 flat 也涉及 Scalar 容器, 所以:

for flat $(1, 2) { }

将只会迭代一次。记住数组把所有东西都存放在 Scalar 容器中, 在数组身上调用 flat 总是和迭代数组自身相同。实际上, 在数组上调用 flat 返回的同一性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 列表一直是计算机的核心部分,因为之前有计算机,在这段时间里,许多恶魔占据了他们的细节。 它们实际上是 Perl 6...
    焉知非鱼阅读 1,764评论 0 0
  • 从匹配中返回值 Match 对象 成功的匹配总是返回一个 Match 对象, 这个对象通常也被放进 $/ 中, (...
    焉知非鱼阅读 1,778评论 0 1
  • 2009 有用的和有意思的循环 让我们来看一个基本的例子. 这是一个最简单清晰的语法的例子.在这并没有使用括号来包...
    焉知非鱼阅读 536评论 0 0
  • Set Series Operator flip_flop.txt 内容如下: 输出: Grammars 只能在叶...
    焉知非鱼阅读 865评论 0 0
  • Perl 哲学 Perl 是一种能“干实事”的语言。它灵活、宽容、可塑。在一名编程能者的手中,它可以 完成几乎所有...
    firefive阅读 1,357评论 1 11