编程之更全面的工具集
— 焉知非鱼叹息。它总是这样,不是吗?
你很快就写完了关于拥有正确的工具如何使某一特定的编码任务变得微不足道…当你意识到,就在隔壁,有一个更好的例子,你可以用它来表达同样的观点。
我在上一篇文章中谈到的"最长初始子路径"的例子是上周每周挑战的第2项挑战。但那周的挑战1让我更清楚地看到,正确的工具可以简化一项任务。
挑战1是找到最小的不是质数的欧几里得数。第 N 个欧几里得数是由前 N 个质数的乘积加上1给出的。所以欧几里得数的序列是:
(2)+1, (2*3)+1, (2*3*5)+1, (2*3*5*7)+1, (2*3*5*7*11)+1, ...
如果有一个简单的方法来建立另一个列表的部分乘积的序列(例如质数的部分乘积),这个序列就很容易计算了:
(2), (2*3), (2*3*5), (2*3*5*7), (2*3*5*7*11), ...
当然,Raku 也有这样一个工具内置在核心语言中。它叫做三角还原元操作符。
在 Raku 中,一个普通的还原运算符(如 [*]
):
$product = [*] @nums;
在列表的每两个元素之间插入方括号内的操作符,就像已经写好的一样:
$product = @nums[0] * @nums[1] * @nums[2] * ... * @nums[*-1];
在这个例子中,这计算的是所有数字的乘积。
另一方面,三角还原元运算符(例如,“三角积"运算符:[/*]
):
@products = [\*] @n;
或多或少地做了同样的事情,但它不是只返回最后的计算结果,而是从每一个连续的乘法步骤中返回一个部分结果的渐进列表,就像它被写的那样:
@products = (@n[0], @n[0]*@n[1], @n[0]*@n[1]*@n[2], ...);
这似乎是一个神秘而无用的功能,但是,当然,这正是我们实际需要的神秘而无用的功能,以便从质数列表中建立欧几里得数的列表。
记住,我们要找的是第一个非质数欧几里得数。也就是:从所有大于从2到无穷大的所有质数的累计部分乘积序列的数中,选出第一个非质数。
而这一描述,又一次直接转化为一行 Raku:
say first !*.is-prime, # Print the first non-prime from
map *+1, # all numbers one greater than
[\*] # the cumulative products of
grep &is-prime, # all the prime numbers
2..Inf; # from 2 to infinity
当然,如果你还不熟悉 Raku,夹在语句中间的 [\*]
可能看起来太可怕了。
可喜的是,我们的解决方案也恰好是纯函数式的,所以很容易就能把违规的行噪声剔除,并将其隐藏在某个名字更柔和的函数中:
sub partial-products-of { [\*] @^list }
然后我们可以这样写:
say first !*.is-prime, # Print the first non-prime from
map *+1, # all numbers one greater than
partial-products-of # the cumulative products of
grep &is-prime, # all the prime numbers
2..Inf; # from 2 to infinity
或者,如果这还是太玄乎了,我们可以全盘托出,把整个事情全部重构成纯英文:
sub successor-of { @^list.map(*+1) }
sub the-primes { (2..Inf).grep(&is-prime) }
sub first-non-prime { @^list.first(!*.is-prime) }
这给了我们:
say first-non-prime successor-of partial-products-of the-primes;
没有比这更多的声明性或自文档化的代码了! . . . . . 当然,除非我们也正好知道欧几里得数的定义:
sub Euclid-number {successor-of partial-products-of the-primes}
在这种情况下,我们的整个解决方案现在只是我们问题的原始描述:
say first-non-prime Euclid-number;
“Raku:这不是你爷爷奶奶的可执行代码行噪音。”
by Damian