NubyGems : Inject is Functional!

by Gregory Brown

When I first found the inject method, it became a new favorite tool.

It was great to be able to turn something like this:


a = [7,8,9]
b = [1,2,3,4]
b.each { |e| a << e + 1}


into something like this:


b = [1,2,3,4]
a = b.inject([7,8,9]) { |s,e| s << e + 1 }


It was cool because it was even possible to build hashes this way, if you used a little trick.


b = [1,2,3,4]
a = b.inject({}) { |s,e| s[e.to_s] = e; s }


This should have given me the red flag though. Why should I need to pass the hash as the last value in the block?

go ahead, try something like this:

b = [1,2,3,4]
a = b.inject([]) { |s,e| s << e if e < 3 }


Now what I would *want* this to do is give me something like a #=> [1,2], but instead it gives us a #=> nil

So we have to do that annoying trick again:

b = [1,2,3,4]
a = b.inject([]) { |s,e| s << e if e < 3; s }


Now, I'm no nuby, but I suppose there is a little bit of nubyism in all of us. I just learned last week why inject was surprising me so much. It's not really just a shortcut for each that gives you a base value to build up, it's a functional method.

For those who aren't familiar with functional programming, there is alway the wikipedia, but the relevant part for this particular problem is that inject isn't designed to do anything destructive. That means things like << and += are bad and things like + are good.

That's why you see the typical sum example of inject using just a + operator


sum = [1,2,3,4].inject(0) { |s,e| s + e } #=> 10


And if you want to build a hash, you can do so without the hack I showed before

hash = [1,2,3,4].inject({}) { |s,e| s.merge( { e.to_s => e } ) }


The reason these bits of code work is because the return value of the block is what becomes the new s, NOT the original parameter you passed to inject.

Now you might say 'hey, this is probably pretty slow, building all these new references and passing them around'. I sort of thought the same thing my self, and am not quite sure how I feel about that.

But the idea is, you're really fighting the function when you use destructive methods. If you find yourself needing to modify the original object rather than build a result set functionally, each isn't THAT ugly. :)

4 Comments

Tim
2006-07-11 21:42:08
inject does look cool, I'll have to start using that:)


Not quite sure what you mean about destructive methods. If you try something like this:


b = [1,2,3,4]
a = b.inject([]) { |s,e| s << e if e < 3; puts s.object_id; s }


it shows that s is the same object passed around. It's probably designed this way for immediate values such as summing with a Fixnum, since you can't pass them by reference.


cheers
Tim

gregory
2006-07-11 21:54:41
<< is a destructive method. You are modifying the object assigned to s.


the only reason why the object id is the same in your example is because you are using inject as if it were not a functional method


You can rewrite this code to be non-destructive:
a = b.inject([]) { |s,e| e < 3 ? s + [e] : s }

Mike
2006-07-12 19:07:24
For that first example, wouldn't the logical thing be


[7,8,9]+[1,2,3,4].map { |x| x+1 }

gregory
2006-07-12 22:52:52
Sure, that's much nicer. I mostly was just pointing the functional stuff about inject, not trying to show the handy ways to use it.


However, the code you just showed is also functional. :)