问题
I started solving exercises in hackerrank in the enumerable section. The exercise asks to complete the sum method which takes an integer n and returns the sum to the n terms of the series. I found the solution from another source but I don't quite understand how the reduce works in this case and the output.
def sum_terms(n)
series = []
1.upto(n) do |i|
series.push(i ** 2 + 1)
end
series.reduce(0, :+)
end
puts sum_terms(5)
# outputs 60
回答1:
We can write this method as follows:
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
The steps are as follows:
n = 5
arr = create_series(n)
#=> [2, 5, 10, 17, 26]
arr.reduce(0, :+)
#=> 60
Let's first look at the method create_series
. This method returns an array of n
elements, those elements being a mapping of the integers 1
, 2
,...,n
. "Mapping" suggests that it would be more sensible to use the method Enumerable#map than creating an empty array (series
), appending n
elements to it and returning that array:
def create_series(n)
1.upto(n).map do |i|
i**2 + 1
end
end
create_series(5)
#=> [2, 5, 10, 17, 26]
Because map
's block is so brief we'd probably write it with braces rather than do..end
:
def create_series(n)
1.upto(n).map { |i| i**2 + 1 }
end
Now let's look at the method sum_terms
. For n = 5
, this becomes:
[2, 5, 10, 17, 26].reduce(0, :+) #=> 60
which is the shorthand version of:
[2, 5, 10, 17, 26].reduce(0) { |tot,x| tot + x) #=> 60
Here I am using the form of Enumerable#reduce (aka inject
) that takes an argument (0
), which is the initial value of of the block variable tot
. When the first element of the array that is reduce
's receiver (2
) is passed to map
's block, the block variable x
is set equal to that value. The block calculation is then performed:
tot + n
#=> 0 + 2 => 2
The value of tot
(0
) is now replaced with that sum (2
). Specifically, the value of the memo (here tot
) is set equal to the last calculation performed in the block. Next, the element 5
of the receiver is passed to the block and x
is set equal to it. The block calculation is now:
tot + n
#=> 2 + 5 => 7
and tot
is set equal to 7
. This is repeated thrice more, causing tot
to successively equal 17
, 34
and 60
. As there are then no more elements to pass to the receiver the block returns the final value of tot
, 60
.
Now consider the following:
[2, 5, 10, 17, 26].reduce(:+) #=> 60
which is shorthand for:
[2, 5, 10, 17, 26].reduce { |tot,x| tot + x } #=> 60
This differs from the first calculation in that reduce
does not have an argument. As explained in the documentation, in this case tot
is initially set equal to the first value of the receiver, 2
, and then each of the four remaining elements of the receiver is passed to the block, causing tot
to successively equal 7
, 17
, 34
and 60
.
Clearly both forms of reduce
give the same result in this case1.
We can improve on this code, however, by skipping the calculation of array [2, 5, 10, 17, 26]
as follows:
1.upto(5).reduce(0) { |tot,i| tot + i**2 + 1 } #=> 60
Notice that reduce
must have an argument of zero here, as
1.upto(5).reduce { |tot,i| tot + i**2 + 1 } #=> 59
is equivalent to:
1 + 2.upto(5).reduce(0) { |tot,i| tot + i**2 + 1 }
which is incorrect.
A simpler way of performing this calculation is to use the method Enumerable#sum, which made its debut in Ruby v2.4:
1.upto(5).sum { |i| i**2 + 1 } #=> 60
Yet simpler is to evaluate Faulhaber's formula:
n = 5
n + n*(n + 1)*(2*n + 1)/6 #=> 60
1 There are situations reduce
is assigned an argument (often zero) merely to deal with a so-called edge case. Suppose, for example, we wished to sum the elements of an array arr
and add that to 10
. We could write 10 + arr.reduce(:+)
which works fine as long as arr
is not empty. 10 + [].reduce(:+)
raises an exception, however, as [].reduce(:+) #=> nil
. By contrast, 10 + [].reduce(0, :+) #=> 10
.
回答2:
Your code works but you should see documentation for how to use reduce
But a simplified version in your use case is just that it sums the integers in the array. You also don't need to pass 0
as a first argument so you can just do:
series.reduce(:+) # instead of series.reduce(0, :+)
回答3:
Basically reduce
performs an operation on every element in the series
array. It takes two arguments.
The operation is defined by the second argument (in this case it's a symbol that names the plus operator :+
). Therefore all elements are added to each other. The first argument specifies an initial value.
You can read more about this here: https://ruby-doc.org/core-2.6.3/Enumerable.html#method-i-reduce
回答4:
This is an absolutely legit question, because unlike all other methods in Enumerable
accepting a block, reduce
(and inject
as its alias) has a weird “short” notation, accepting a method name as a second argument without &
.
So, if it was literally any other method, accepting a block, it was written as series.reduce(0, &:+)
.
Which is a shorthand to series.reduce(0) { |e, acc| e + acc }
.
Sidenote: the code is extremely not ruby idiomatic, I’d rewrite it like
def sum_terms(n)
1.upto(n).map do |i|
i ** 2 + 1
end.reduce(0, &:+)
end
来源:https://stackoverflow.com/questions/57441885/requesting-an-insight-on-reduce-ruby-code