Raku 编程/惰性列表和馈送
在大多数传统的计算系统中,数据对象被分配到一个固定的尺寸,并且它们的数值被填充到内存中的空间。例如,在C中,如果我们声明一个数组int a[10]
,那么数组a
将是一个固定大小,有足够的空间来精确地存储 10 个整数。如果我们想要存储 100 个整数,我们需要分配一个 100 个的空间。如果我们想要存储一百万个,我们需要分配一个那样的尺寸的数组。
让我们考虑一个问题,我们想要计算一个乘法表,一个二维数组,其中数组中给定单元格的值是它两个索引的乘积。以下是一个简单的循环,可以用它来生成具有高达 N 个因子的表
int products[N][N];
int i, j;
for(i = 0; i < N; i++) {
for(j = 0; j < N; j++) {
products[i][j] = i * j;
}
}
创建这个表可能需要一段时间来执行所有N2个操作。当然,一旦我们初始化了表,在其中查找一个值的速度就非常快。这里要考虑的另一个问题是,我们最终计算的值比我们实际使用的多,所以这是浪费的努力。
现在,让我们看看一个执行相同操作的函数
int product(int i, int j) {
return i * j;
}
这个函数不需要任何启动时间来初始化它的值,但它确实需要每次调用时额外的计算时间来计算结果。它的启动速度比数组快,但在每次访问时比数组花费的时间更多。
结合这两个想法,我们得到了惰性列表。
惰性列表就像数组,但有一些主要的差异
- 它们不一定要用预定义的大小来声明。它们可以是任何大小,甚至无限长。
- 它们只在需要时计算它们的数值,并且只在需要时计算所需的值。
- 一旦它们的数值被计算出来,它们就可以被存储起来以供快速查找。
与惰性列表相反的是积极列表。积极列表立即计算并存储所有它们的值,就像 C 中的数组一样。积极列表不能是无限长的,因为它们需要在内存中存储它们的值,而计算机没有无限的内存。
Raku 同时拥有两种类型的列表,并且它们在内部处理,无需程序员干预。可以是惰性的列表将被惰性地处理。不能是惰性的列表将被积极地计算和存储。惰性列表在存储空间和计算开销方面提供了优势,因此 Raku 尝试默认使用它们。Raku 还提供了一些结构,可以用来支持惰性和提高列表计算的性能。
我们已经看到了范围。范围默认情况下是惰性的,这意味着范围中的所有值不一定会在你将它们分配给数组时被计算出来
my @lazylist = 1..20000; # Doesn't calculate all 20,000 values
由于它们的惰性,范围甚至可以是无限的
my @lazylist = 1..Inf; # Infinite values!
迭代器是特殊的数据库项,它们一次遍历一个复杂的数据对象中的一个元素。想想文本编辑器程序中的光标;光标读取一次按键,将字符插入到它当前的位置,然后移动到下一个位置等待下一个按键。通过这种方式,可以一次插入一个很长的字符数组,而你,编辑器,不必手动移动光标。
以同样的方式,Raku 中的迭代器自动遍历数组和散列表,自动跟踪你在数组中的当前位置,这样你就不必自己去跟踪了。我们之前讨论循环时已经看到了迭代器的一个用法,尽管我们没有用“迭代器”这个名字来称呼它们。以下两个循环执行相同的函数
my @x = 1, 2, 3, 4, 5;
loop(my int $i = 0; $i < @x.elems; $i++) {
@x[$i].say;
}
for @x { # Same, but much shorter!
$_.say;
}
第一个循环使用$i
变量手动遍历@x
数组,跟踪当前位置,并使用$i < @x.length
测试来确保我们还没有到达末尾。在第二个循环中,for
关键字为我们创建了一个迭代器。迭代器自动跟踪我们在数组中的当前位置,自动检测我们何时到达数组的末尾,并自动将每个后续的值加载到$_
默认变量中。值得一提的是,我们可以使用一些 Raku 的惯用法来使它更短
.say for @x;
迭代器是任何实现了Iterator
角色的对象。我们稍后会谈到角色,但现在说一个角色是一个标准接口,其他类可以参与其中就足够了。因为它们可以是任何类,只要它有一个标准接口,迭代器就可以做我们定义它们做的事情。迭代器可以轻松地遍历数组和散列表,但专门定义的类型也可以遍历树、图、堆、文件和所有其他数据结构和概念。
如果一个数据库项有一个关联的迭代器类型,它可以通过.Iterator()
方法访问。此方法在大多数情况下被诸如for
循环之类的结构在内部调用,但如果你真的需要,你可以访问它。
馈送提供了一种很好的图形化方式来显示数据在复杂赋值语句中的移动位置。馈送有两个端点,一个“钝”端和一个“尖”端。钝端连接到一个数据源,它是一个值列表。尖端连接到一个接收器,接收器可以一次接收至少一个元素。馈送可以用来从右到左或从左到右发送数据,这取决于馈送指向的方向。
my @x <== 1..5;
say @x # 1, 2, 3, 4, 5
@x ==> @y ==> print # 1, 2, 3, 4, 5
say @y # 1, 2, 3, 4, 5
分层馈送将数据从一个馈送移动到另一个馈送。但是,有两个点的馈送将追加到馈送链中的最后一个项目
my @x = 1..5;
@x ==> map {$_ * 2} ==> @y;
say @x; # 1, 2, 3, 4, 5
say @y; # 2, 4, 6, 8, 10
@x ==>>
@y ==> @z;
say @z # 1, 2, 3, 4, 5, 2, 4, 6, 8, 10
我们可以使用gather
和take
关键字编写我们自己的迭代器类型。这两个关键字的行为与我们之前见过的尖块非常相似。但是,与尖块不同的是,gather/take 可以返回值。与尖块一样,gather/take 可以与循环结合起来形成自定义迭代器。
gather
用来定义一个特殊的块。该块的代码可以执行任意计算,并使用take
返回一个值。以下是一个例子
my $x = gather {
take 5;
}
say $x; # 5
这本身没什么用。但是,我们现在可以将它与循环结合起来返回一个很长的值列表
my @x = gather for 1..5 {
take $_ * 2;
}
say @x # 2, 4, 6, 8, 10
take
运算符执行两个动作:它获取它传递的值的捕获,并将其作为gather
块结果之一返回,并且它返回它被传递以供存储的值。我们可以很容易地将这种行为与state
变量结合起来,以递归地使用值。
my @x = gather for 1..5 {
state $a = $_;
$a = take $_ + $a;
}
say @x; # 2, 4, 7, 11, 16