The usual way of doing that would be this:
def sum(nums) nums.reduce(&:+) end
which is short for something like this:
def sum(nums) nums.reduce(0) { |total, num| total + num } end
I see that Neil posted a similar solution while I was typing this, so I'll just note that reduce
and inject
are two names for the same method - Ruby has several aliases like this so that people used to different other languages can find what they're looking for. He also left off the &
, which is optional when using a named method for reduce
/inject
, but not in other cases.
Explanation follows.
In Ruby you don't normally use explicit loops (for
, while
, etc.). Instead you call methods on the collection you're iterating over, and pass them a block of code to execute on each item. Ruby's syntax places the block after the arguments to the method, between either do
...end
or {
...}
, so it looks like traditional imperative flow control, but it works differently.
The basic iteration method is each
:
[1,2,3].each do |i| puts i end
That calls the block do |i| puts i end
three times, passing it 1, then passing it 2, and finally passing it 3. The |i|
is a block parameter, which tells Ruby where to put the value(s) passed into the block each time.
But each
just throws away the return value of the block calls (in this case, the three nil
s returned by puts
). If you want to do something with those return values, you have to call a different method. For example, map
returns an array of the return values:
[1,2,3].map do |i| puts i end
#=> [nil, nil, nil]
That's not very interesting here, but it becomes more useful if the block returns something:
[1,2,3].map do |i| 2*i end
#=> [2,4,6]
If you want to combine the results into a single aggregate return value instead of getting back an array that's the same size as the input, that's when you reach for reduce
. In addition to a block, it takes an extra argument, and the block itself is also called with an extra argument. The extra parameter corresponding to this argument is called the "accumulator"; the first time the block is called, it gets the argument originally passed to reduce
, but from then on, it gets the return value of the previous call to the block, which is how each block call can pass information along to the next.
That makes reduce
more general than map
; in fact, you can build map
out of reduce
by passing in an empty array and having the block add to it:
[1,2,3].reduce([]) do |a,i| a + [2*i] end
#=> [2,4,6]
But since map
is already defined, you would normally just use it for that, and only use reduce
to do things that are more, well, reductive:
[1,2,3].reduce(0) do |s, i| s + 2*i end
#=> 12
...which is what we're doing in solving your problem.
Neil and I took a couple extra shortcuts. First, if a block does nothing but call a single method on its parameters and return the result, you can get an equivalent block by prefixing &:
to the method name. That is, this:
some_array.reduce(x) do |a,b| a.some_method(b) end
can be rewritten more simply as this:
some_array.reduce(x, &:some_method)
and since a + b
in Ruby is really just a more-familiar way of writing the method call a.+(b)
, that means that you can add up numbers by just passing in &:+
:
[1,2,3].reduce(0, &:+)
#=> 6
Next, the initial accumulator value for reduce
is optional; if you leave it out, then the first time the block is called, it gets the first two elements of the array. So you can leave off the 0
:
[1,2,3].reduce(&:+)
#=> 6
Finally, you normally need the &
any time you are passing in a block that is not a literal chunk of code. You can turn blocks into Proc objects and store them in variables and in general treat them like any other value, including passing them as regular arguments to method calls. So when you want to use one as the block on a method call instead, you indicate that with the &
.
Some methods, including reduce
, will also accept a bare Symbol (like :+
) and create the Proc/block for you; and Neil took advantage of that fact. But other iterator methods, such as map
, don't work that way:
irb(main):001:0> [-1,2,-3].map(:abs)
ArgumentError: wrong number of arguments (1 for 0)
from (irb):1:in `map'
from (irb):1
from /usr/bin/irb:12:in `<main>'
So I just always use the &
.
irb(main):002:0> [-1,2,-3].map(&:abs)
#=> [1, 2, 3]
There are lots of good online tutorials for Ruby. For more general information about map/reduce and related concepts, and how to apply them to problem-solving, you should search for introductions to "functional programming", which is called that because it treats "functions" (that is, blocks of executable code, which in Ruby are realized as Proc
objects) as values just like numbers and strings, which can be passed around, assigned to variables, etc.