arraysrubyfixnum

Array interpreted as a Fixnum


I'm currently learning ruby and I wrote this piece of code :

def multi_gen
  s = []
  for i in (3..10)
    if i%3 == 0 || i%5 == 0
      s<<i
    end
  end
  return s
end

puts multi_gen

def rec_sum(num_arr)
  if num_arr == []
    return 0
  else
    num_arr.first + rec_sum(num_arr.shift)
  end
end

puts rec_sum(multi_gen)

That should return the sum of all 3 and 5 multiples up to 1000.

But I get an error :

myrbfile.rb:17:in `rec_sum': undefined method `first' for 3:Fixnum (NoMethodError)
        from villani.rb:17:in `rec_sum'
        from villani.rb:21:in `<main>'

But when I re-write it like this :

def multi_gen
  s = []
  for i in (3..10)
    if i%3 == 0 || i%5 == 0
      s<<i
    end
  end
  return s
end

puts multi_gen

def rec_sum(num_arr)
  if num_arr == []
    return 0
  else
    num_arr[0] + rec_sum(num_arr[1..num_arr.last])
  end
end

puts rec_sum(multi_gen)

I don't get the error.

So why is my first rec_sum functions interpretting my Array as a Fixnum in the first case?


Solution

  • mudasobwa already explained why using shift doesn't give the expected result. Apart from that, your code is somehow unidiomatic.

    In multi_gen you are creating an empty array and append elements to it using a for loop. You rarely have to populate an array manually. Instead, you can usually use one of Ruby's Array or Enumerable methods to generate the array. select is a very common one – it returns an array containing the elements for which the given block returns true:

    (1..1000).select { |i| i % 3 == 0 || i % 5 == 0 }
    #=> [3, 5, 6, 9, 10, 12, ...]
    

    In rec_sum, you check if num_arr == []. Although this works, you are creating an empty throw-away array. To determine whether an array is empty, you should call its empty?:

    if num_arr.empty?
      # ...
    end
    

    To get the remaining elements from the array, you use:

    num_arr[1..num_arr.last]
    

    which can be abbreviated by passing a negative index to []:

    num_arr[1..-1]
    

    There's also drop which might look a little nicer:

    num_arr[0] + rec_sum(num_arr[1..-1])
    # vs
    num_arr.first + rec_sum(num_arr.drop(1))
    

    Another option to get first and remaining elements from an array is Ruby's array decomposition feature (note the *):

    def rec_sum(num_arr)
      if num_arr.empty?
        0
      else
        first, *remaining = num_arr
        first + rec_sum(remaining)
      end
    end
    

    You could also consider using a guard clause to return from the method early:

    def rec_sum(num_arr)
      return 0 if num_arr.empty?
      first, *remaining = num_arr
      first + rec_sum(remaining)
    end
    

    Writing recursive methods is great for learning purposed, but Ruby also has a built-in sum method:

    multi_gen.sum #=> 234168
    

    or – since you are using an older Ruby version – inject:

    multi_gen.inject(0, :+) #=> 234168