4

Q: Write a method, sum which takes an array of numbers and returns the sum of the numbers.

A:

def sum(nums)
  total = 0

  i = 0
  while i < nums.count
    total += nums[i]

    i += 1
  end

  # return total
  total
end

There has to be another way to solve this without using while, right? Anyone know how?

Edit: This is not an exam or test. This is a practice problem provided on github for app academy. They provide the question and answer as an example. I just read however that good programmers don't like to use while or unless, so I was curious if I could learn something to solve this problem a better way. Like with enumerable? (Noob at Ruby here, obviously..)

Also, I would love any walkthrough or methods that I should learn.. This question is also different because I am asking for specific examples using this data.

rubygirl
  • 73
  • 1
  • 5

2 Answers2

11

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 nils 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.

Mark Reed
  • 91,912
  • 16
  • 138
  • 175
  • Ahh! Okay this is exactly the kind of thing I need to learn. I don't know anything about inject or reduce yet. Thank you! What can I research to learn more about how to to solve problems like this? I'm learning anything I can get my hands on about Ruby right now! – rubygirl Apr 02 '14 at 17:34
  • Oh my gosh. I cannot THANK YOU ENOUGH!!! That is exactly what I was looking for!!! Thank you, thank you, thank you! I'm actually printing out your answer as we speak to study. I am completely blown away and I appreciate this more than I can say. I was honestly feeling so lost learning this, and actual input from a human being about what I should be researching and learning is so above and beyond helpful. THANK YOU THANK YOU THANK YOU!!!!!!!!!!!!!!!!!!! – rubygirl Apr 02 '14 at 18:43
  • Glad I could help. I added some extra information about how to get from where I left off to the solutions that Neil and I posted. – Mark Reed Apr 02 '14 at 19:19
  • I swear I was about five minutes away from giving up on Ruby forever and pursuing a different career path before you answered this question. Thank you again. – rubygirl Apr 02 '14 at 19:28
  • Oh, and while you're certainly welcome, instead of saying 'thanks' in a comment, the usual way to express appreciation for an answer on SO is to accept it. :) – Mark Reed Apr 02 '14 at 20:55
  • Oops! Just figured that out and did it! Thank you again!!!!!!!!!!! – rubygirl Apr 03 '14 at 17:56
7

Probably the most idiomatic way of doing this in Ruby is:

nums.inject(:+)

. . . although this basically hides all the working, so it depends what the test is trying to test.

Documentation for Array#inject

Neil Slater
  • 26,512
  • 6
  • 76
  • 94
  • Thank you for the link! I really appreciate it! I know next to nothing about inject and I want to learn more. Still learning the basic things about ruby right now. – rubygirl Apr 02 '14 at 17:36