Perl 6使用reduce一次性计算整数数组的平均值

11

我正在尝试使用reduce函数一步计算整数数组的平均值。 我不能这样做:

say (reduce {($^a + $^b)}, <1 2 3>) / <1 2 3>.elems;

因为它将平均值计算分成了两个部分。

我需要像这样做:

say reduce {($^a + $^b) / .elems}, <1 2 3>;

但是当然这样行不通。

如何一步完成?(使用 map 或其他函数都可以。)


给出一个例子:given <1 2 3> { say (reduce {($^a + $^b)}, $_ >>/>> $_.elems) }; - Håkon Hægland
可以。不过最后一部分(特别是>>/>>的使用)需要一些解释。 - Terry
它被称为超级运算符,有关更多信息,请参见超级运算符 - Håkon Hægland
2个回答

12

TL;DR 这篇回答首先介绍了一种等效代码的习惯用法,然后讨论了P6风格的“暗示”编程和增加简洁性。 我还添加了关于Håkon ++在您问题的第一个评论中使用的超级运算符的“额外”脚注。5

也许不是你想要的,但是初步的惯用解决方案

我们将从一个简单的解决方案开始。1

P6有内置例程2可以实现你所要求的功能。这是使用内置的sub的方法:

say { sum($_) / elems($_) }(<1 2 3>); # 2

下面是使用相应的{{3}}方法

say { .sum / .elems }(<1 2 3>); # 2

函数式编程是什么?

首先,让我们将.sum替换为显式的归约:

.reduce(&[+]) / .elems

当在P6中的表达式开头使用&时,您知道该表达式将first class citizen作为一个Callable引用。

将中缀+运算符称为函数值的一种冗长的方式是&infix:<+>。简写方式是&[+]

如您所知,reduce例程将二元操作作为参数,并将其应用于值列表。在方法形式(invocant.reduce)中,"invocant"是列表。

上述代码调用两个没有显式调用者的方法——.reduce.elems。这是一种"tacit" programming的形式;以这种方式编写的方法隐含地(或"tacitly")使用$_(也称为“the topic”或简称“it”)作为它们的调用者。

主题化(明确建立“it”是什么)

given关键字将一个值绑定到$_(也称为"it")用于单个语句或块。

(这就是given的全部作用。许多其他关键字也可以将其置于顶层,但它们还会执行其他操作。例如,for一系列值绑定到$_,而不仅仅是一个。)

因此,您可以编写:

say .reduce(&[+]) / .elems given <1 2 3>; # 2

或者:

$_ = <1 2 3>;
say .reduce(&[+]) / .elems; # 2

但是,考虑到你的重点是函数式编程,还有另外一种方法你应该知道。

代码块和"it"

首先,将代码包装在一个代码块中:

{ .reduce(&[+]) / .elems }

上面是一个Block,因此是一个lambda表达式。没有签名的lambda表达式会获得一个默认的signature,它接受一个可选参数。
现在我们可以再次使用given,例如:
say do { .reduce(&[+]) / .elems } given <1 2 3>; # 2

但我们也可以使用普通的函数调用语法:

say { .reduce(&[+]) / .elems }(<1 2 3>)

因为后缀 (...) 调用了左侧的 Callable,且在上述情况下将一个参数传递给期望一个参数的块中的括号,结果与前一行代码中的 do 4given 相同。

内置函数的简洁性

以下是另一种编写方式:

<1 2 3>.&{.sum/.elems}.say; #2

这将会像调用方法一样调用一个代码块。如果你已经掌握了P6的基础知识,那么这仍然非常易读。

或者你可以开始变得有点儿滑稽:

<1 2 3>.&{.sum/$_}.say; #2

如果你懂P6的话,这段文字还是可读的。 / 是一个数字(除法)运算符。数字运算符会强制将其操作数转换为数字。在上面的例子中,$_ 绑定到一个列表 <1 2 3>。在Perl中,集合在数字上下文中的值是其元素数量。

调整P6以适应您的需求

到目前为止,我一直坚持使用标准P6。

当然,你可以编写任何Unicode字母命名的submethod。如果你想要sumelems的单个字母别名,那就去吧:

my (&s, &e) = &sum, &elems;

但是您也可以根据需要扩展或更改语言。例如,您可以创建用户定义的运算符:

#| LHSRHS.
#| LHS is an arbitrary list of input values.
#| RHS is a list of reducer function, then functions to be reduced.
sub infix:<⊛> (@lhs, *@rhs (&reducer, *@fns where *.all ~~ Callable)) {
  reduce &reducer, @fns».(@lhs)
}
say <1 2 3> ⊛ (&[/], &sum, &elems); # 2

我现在不打算解释这个。 (请在评论中随意提问。)我的重点只是强调您可以引入任意(前缀,中缀,环绕等)运算符。

如果自定义运算符不足够,您可以更改任何其他语法。 cf "braid"

脚注

1 这就是我通常编写代码以执行问题所要求的计算的方式。 @timotimo++的评论促使我改变了我的演示方式,先从那里开始,然后转向专注于更FPish的解决方案。

2 在P6中,所有内置函数都称为“例程”,并且是Routine的子类的实例 - 通常是SubMethod

3 并非所有内置的sub例程都有相应命名的method例程。反之亦然。相反,有时候存在相应命名的例程,但它们的工作方式并不完全相同(最常见的区别是sub的第一个参数是否与方法形式中的“调用者”相同)。此外,您可以使用语法.&foo(对于命名的Sub)或.&{ ... }(对于匿名的Block像调用方法一样调用子例程,或者使用语法foo invocant:foo invocant: arg2, arg3(如果它具有超出调用者的参数)以类似于子例程调用的方式调用方法foo

4 如果块被用在显然应该调用它的地方,那么它会被调用。如果它没有被调用,则可以使用显式的do语句前缀来调用它。

Håkon在你的问题中首次提到了“超级运算”。只需使用一个易于识别和记忆的“metaop”(一元操作)或一对它们(二元操作),hyperoperations就可以将操作分配给数据结构的所有“叶子”(对于一元操作),或者基于成对的数据结构的“叶子”进行配对创建新的数据结构(对于二元操作)。注意,超级运算是并行执行的7
对于超级运算而言,“叶子”的定义取决于应用的操作(请参阅the is nodal trait)以及特定元素是否为Iterable
超级运算至少在语义上是并行执行的。超级运算假设8,“叶子”上的操作没有相互干扰的副作用,也就是说,将操作应用于一个“叶子”时产生的任何副作用都可以安全地忽略,不影响将操作应用于任何其他“叶子”。

8通过使用超级操作,开发者声明没有有意义的副作用这一假设是正确的。编译器将按照此假设进行操作,但不会检查其是否为真。从安全角度来看,这就像带有条件的循环。即使结果是无限循环,编译器也会遵循开发者的指示。


3
别忘了还有 .sum 这个方法,它比 .reduce(&[+]) 更加简短,也许更加清晰易懂。 - timotimo
2
@timotimo 是啊,那是我的第一反应,但是...好吧,不过话说回来,我已经重新写了我的答案。感谢你的提醒。 :) - raiph
2
@raiph 是的,我正在寻找最短且最具表现力的功能性方法来执行列表操作。 “一次性”实际上就是这个意思 :) 将“do”和“given”二元组变形为普通函数调用,在这种情况下我第一次看到它,而且非常有用。 - Terry
1
说 {([+] $) / + $}(<1 2 3>) ... 如果你更喜欢简洁而不是易读性 - librasteve

7

看起来,我猜 given 关键字就是这个技巧的关键。因为它允许在一步中完成计算 :) - Terry
实际上,你甚至不需要 .elemsgiven @foo { ([+] $_) / $_ };可以正常工作(如果我们可以假设它已定义,则使用 with 也可以)。 - user0721090601
借鉴@guifa的评论:正如@raiph所解释的那样,这是因为“/”是一个数字运算符,因此它在数字上下文中解释其操作数。在这里,“$_”是一个列表,其数字值只是它所包含的元素数量(即“.elems”)。更多信息:https://docs.perl6.org/language/contexts#Number - uzluisf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接