Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use next with inject in ruby

I was writing some code like following:

[1,2,3,4,5].inject([]) do |res, a|
  res << a*a and next if a == 2
  res << a
end

It gives following error:

NoMethodError: undefined method `<<' for nil:NilClass

As by next it makes res variable as nil, How to work around this problem?

I tried various ways but couldn't get next to work with ruby, I know this snippet I have provided can be done without next(a == 4 ? res << a*a : res << a), but in my actual use-case I have some complex logic and can't be done that simply.

like image 306
Saurabh Avatar asked Nov 09 '14 18:11

Saurabh


2 Answers

Ugh. Don't use a trailing conditional for this. Instead it is easily done using a standard if/else:

[1,2,3,4,5].inject([]) do |res, a|
  if a == 2
    res << a*a
  else
    res << a
  end
end
# => [1, 4, 3, 4, 5]

Any time you feel like you're coding yourself into a corner, don't look for a way out, instead, back up and look at what you're trying to accomplish to see if there is a more straightforward way there.

I'd probably tweak the code a bit more though, for readability. Long term maintenance relies on quickly understanding what is going on, and code that is convoluted or not obvious can take its toll later:

[1,2,3,4,5].inject([]) do |res, a|
  if a == 2
    res << a*a
  else
    res << a
  end

  res # return it for clarity in what the block is returning
end
# => [1, 4, 3, 4, 5]

inject is similar to each_with_object, only it relies on the accumulator being returned at the end of the block, which is why I would add the res at the end of the block for clarity, due to the if block. Switching to each_with_object removes that reliance on the return value of the block, allowing the following code to be more logically clear:

[1,2,3,4,5].each_with_object([]) do |a, ary|
  if a == 2
    ary << a*a
  else
    ary << a
  end
end
# => [1, 4, 3, 4, 5]

Of course, at that point, the whole thing can be reduced further, and could take advantage of the ternary version using:

[1,2,3,4,5].each_with_object([]) do |a, ary|
  a2 = (a == 2) ? a * a : a
  ary << a2
end
# => [1, 4, 3, 4, 5]

Which of the above two are more readable is somewhat up to the person coding it and the person responsible for maintaining it. I'd lean toward the non-ternary version because it's more easily extended/expanded and doesn't have the line noise of the ternary ?: chain.


Since it was asked in the comments, map reduces some noise, and is how we should transform an array:

[1,2,3,4,5].map { |a|
  (a == 2) ? a * a : a
}

That's untested but it looks correct.

like image 175
the Tin Man Avatar answered Nov 03 '22 15:11

the Tin Man


Replace the

res << a*a and next if a == 2

with

next res << a*a if a == 2

Now, it will work.

Example :-

#!/usr/bin/env ruby

ar = [1,2,3,4,5].inject([]) do |res, a|
  next res << a*a if a == 2
  res << a
end

p ar
# >> [1, 4, 3, 4, 5]

Read the documentation of next

next can take a value, which will be the value returned for the current iteration of the block....

like image 45
Arup Rakshit Avatar answered Nov 03 '22 13:11

Arup Rakshit