Ruby’s inject has a design that can lead to hard to find bugs. The
problem is that you don’t have to specify what is summed; the value of
the block is summed. That means ‘next’ can lead to a bug. Here’s an
example:
No problem:
arr.inject(0) do |sum, i|
sum += i
done
But suppose you need to skip some elements, so you add a ‘next’
statement.
Problem:
arr.inject(0) do |sum, i|
next if (i==3)
sum += i
done
This breaks. If i is 3, then when the next occurs, the value of the
block is nil. The value of the block is added to sum, but because “+”
isn’t defined for nil, there’s an exception. Note that this isn’t due to
the line “sum += i”; it’s due to the design of inject: the value of the
block is added to sum.
The real problem is that ‘inject’ has two semantics: 1) it adds onto sum
using an explicit “sum +=” or 2) is adds the value of the block. A
better approach would be allow only one way to accumulate. That way, you
can’t make a change that inexplicitly changes the function from one
semantics to another.
I agree using next with inject can be dangerous and probably should be
avoided. You can however get round it by just returning the original
value of sum when the condition is met:
arr.inject do |sum,i|
(sum==3) ? sum : sum + i
end
You should use a ‘+’ instead of a ‘+=’. The return value of the block
will be assigned to the next iteration’s value for ‘sum’, so it
doesn’t make sense to modify ‘sum’ inside the block.
But suppose you need to skip some elements, so you add a ‘next’
statement.
Problem:
arr.inject(0) do |sum, i|
next if (i==3)
sum += i
done
Try it like this:
arr.inject(0) do |sum, i|
if i == 3
sum
else
sum + i
end
end
Any time you want to skip an element, simply return ‘sum’. This will
carry the value from the last iteration on over to the next
iteration, doing nothing with the current element. As far as I can
see, you should never have to use ‘next’ in an inject block, unless
you really want the intermediate result to be nil.
In this case I would not even resort to #next. This seems much more
straightforward:
arr.inject(0) {|sum, i| i == 3 ? sum : sum + i}
or, if you do not like the ternary operator
arr.inject(0) {|sum, i| if i == 3 then sum else sum + i end}
IMHO #next is best used if the block is /long/ and you want to short
circuit to the next iteration. If it is a short block like in this case #next does not bring any benefits and in fact makes it more complicated
to understand - at least in my opinion.
arr.inject(0) { |sum, i| sum += i }
arr.inject(0) { |sum, i| sum + i }
both produce the same result. Yuk.
Maybe it should be posted somewhere as a potential ‘gotcha’.
I think it could be that I’m just too familiar with the whole
accumulator thing, but I’m having trouble even imagining a case where
you’d want it any other way. Why would you want those to produce
different results, and - here I can’t even guess - what would those
different results actually be?
I have a hunch that the key phrase is “Provided you know that the
block result is what matters, you can stay out of trouble.” I mean,
that’s sort of inherent in the construct. If you don’t know that,
I’d you’re going to have a lot of trouble with #inject.
block is nil. The value of the block is added to sum, but because “+”
isn’t defined for nil, there’s an exception. Note that this isn’t due to
the line “sum += i”; it’s due to the design of inject: the value of the
block is added to sum.
The real problem is that ‘inject’ has two semantics: 1) it adds onto sum
using an explicit “sum +=” or 2) is adds the value of the block. A
better approach would be allow only one way to accumulate. That way, you
can’t make a change that inexplicitly changes the function from one
semantics to another.
this last part is dead wrong, the semantics of inject have nothing
to do
with summing, or even accumulating anything. the semantics of
inject is
merely that it iterates an enumerable, on the first iteration it passes
arr.inject(0) do |sum, i|
next if (i==3)
sum += i
done
Others have already posted clarifying the semantics of inject and
proposing alternate solutions, so I won’t bother with that. I just
wanted to point out another alternate:
arr.select{ |i| i != 3 }.inject(0){ |sum, i| sum + i }
I like to use block-taking functions on Enumerables like unix pipes
and let the body of each block do the simplest thing possible.
The only potential drawback to breaking the filter apart like this is
that the current implementation of ruby turns this into two loops. If
your array is large, this can be a problem. But for most cases where
you’re not doing number crunching, the inefficiency is negligible.
Ruby’s inject has a design that can lead to hard to find bugs. The
problem is that you don’t have to specify what is summed; the value of
the block is summed. That means ‘next’ can lead to a bug. Here’s an
example:
There were lots of good responses to this post already, but if you’re
interested, I blogged about this a while ago.
----- Original Message -----
From: “Gary B.” [email protected]
Newsgroups: comp.lang.ruby
To: “ruby-talk ML” [email protected]
Sent: Wednesday, November 15, 2006 1:46 AM
Subject: Design problem with ‘inject’
done
This breaks. If i is 3, then when the next occurs, the value of the