我们可以这样写这个方法:
def sum_terms(n)
arr = create_series(n)
arr.reduce(0, :+)
end
def create_series(n)
series = []
1.upto(n) do |i|
series.push(i**2 + 1)
end
series
end
sum_terms(5)
#=> 60
步骤如下:
n = 5
arr = create_series(n)
#=> [2, 5, 10, 17, 26]
arr.reduce(0, :+)
#=> 60
我们先来看create_series这个方法。此方法返回n 元素的数组,这些元素是整数1、2、...、n 的映射。 “映射”表明使用方法 Enumerable#map 比创建一个空数组 (series)、将 n 元素附加到它并返回该数组更明智:
def create_series(n)
1.upto(n).map do |i|
i**2 + 1
end
end
create_series(5)
#=> [2, 5, 10, 17, 26]
因为map 的代码块非常简短,我们可能会用大括号而不是do..end 来编写它:
def create_series(n)
1.upto(n).map { |i| i**2 + 1 }
end
现在让我们看看方法sum_terms。对于n = 5,变为:
[2, 5, 10, 17, 26].reduce(0, :+) #=> 60
这是以下的简写形式:
[2, 5, 10, 17, 26].reduce(0) { |tot,x| tot + x) #=> 60
这里我使用Enumerable#reduce(又名inject)的形式,它接受一个参数(0),这是块变量tot的初始值。当 reduce 的接收者 (2) 的数组的第一个元素被传递到 map 的块时,块变量 x 被设置为等于该值。然后进行块计算:
tot + n
#=> 0 + 2 => 2
tot (0) 的值现在替换为该总和 (2)。具体来说,memo(此处为tot)的值设置为等于块中最后执行的计算。接下来,将接收者的元素5 传递给块,并将x 设置为等于它。块计算现在是:
tot + n
#=> 2 + 5 => 7
并且tot 设置为等于7。这样重复三次,导致tot 依次等于17、34 和60。由于没有更多元素可以传递给接收者,因此块返回 tot、60 的最终值。
现在考虑以下几点:
[2, 5, 10, 17, 26].reduce(:+) #=> 60
简写为:
[2, 5, 10, 17, 26].reduce { |tot,x| tot + x } #=> 60
这与第一个计算的不同之处在于reduce 没有参数。如文档中所述,在这种情况下,tot 最初设置为等于接收器的第一个值,2,然后接收器的四个剩余元素中的每一个都传递给块,导致tot依次等于7、17、34 和60。
显然reduce 的两种形式在这种情况下给出相同的结果1。
不过,我们可以改进这段代码,跳过数组[2, 5, 10, 17, 26] 的计算,如下所示:
1.upto(5).reduce(0) { |tot,i| tot + i**2 + 1 } #=> 60
注意reduce 必须在这里有一个零参数,因为
1.upto(5).reduce { |tot,i| tot + i**2 + 1 } #=> 59
相当于:
1 + 2.upto(5).reduce(0) { |tot,i| tot + i**2 + 1 }
这是不正确的。
执行此计算的一种更简单的方法是使用方法Enumerable#sum,它在 Ruby v2.4 中首次亮相:
1.upto(5).sum { |i| i**2 + 1 } #=> 60
更简单的是评估Faulhaber 公式:
n = 5
n + n*(n + 1)*(2*n + 1)/6 #=> 60
1 在某些情况下,reduce 被分配一个参数(通常为零)只是为了处理所谓的边缘情况。例如,假设我们希望对数组arr 的元素求和并将其添加到10。我们可以写10 + arr.reduce(:+),只要arr 不为空,它就可以正常工作。但是,10 + [].reduce(:+) 会引发异常,例如 [].reduce(:+) #=> nil。相比之下,10 + [].reduce(0, :+) #=> 10.