12
a = [[1, 'a'],[2, 'b'],[3, 'c'], [4, 'd']]
a.inject({}) {|r, val| r[val[0]] = val[1]}

When I run this, I get an index error

When I change the block to

a.inject({}) {|r, val| r[val[0]] = val[1]; r}

It then works.

How is ruby handling the first inject attempt that isn't getting what I want?
Is there a better way to do this?

1
  • +1 For asking a question exactly relevant to something I was trying to do, thus being helpful. ;) Commented May 13, 2013 at 15:12

4 Answers 4

13

Just because Ruby is dynamically and implicitly typed doesn't mean that you don't have to think about types.

The type of Enumerable#inject without an explicit accumulator (this is usually called reduce) is something like

reduce :: [a] → (a → a → a) → a

or in a more Rubyish notation I just made up

Enumerable[A]#inject {|A, A| A } → A

You will notice that all the types are the same. The element type of the Enumerable, the two argument types of the block, the return type of the block and the return type of the overall method.

The type of Enumerable#inject with an explicit accumulator (this is usually called fold) is something like

fold :: [b] → a → (a → b → a) → a

or

Enumerable[B]#inject(A) {|A, B| A } → A

Here you see that the accumulator can have a different type than the element type of the collection.

These two rules generally get you through all Enumerable#inject-related type problems:

  1. the type of the accumulator and the return type of the block must be the same
  2. when not passing an explicit accumulator, the type of the accumulator is the same as the element type

In this case, it is Rule #1 that bites you. When you do something like

acc[key] = value

in your block, assignments evaluate to the assigned value, not the receiver of the assignment. You'll have to replace this with

acc.tap { acc[key] = value }

See also Why Ruby inject method cannot sum up string lengths without initial value?


BTW: you can use destructuring bind to make your code much more readable:

a.inject({}) {|r, (key, value)| r[key] = value; r }
Sign up to request clarification or add additional context in comments.

2 Comments

Oh, so inject is a fold operation. That makes sense then.
Yes, inject is Ruby's (actually, Smalltalk's) name for fold / reduce. BTW: It's refreshing to not get a blank stare when saying "inject is just a fold" :-)
9

There is an easier way -

a = [[1, 'a'],[2, 'b'],[3, 'c'], [4, 'd']]
b = Hash[a] # {1=>"a", 2=>"b", 3=>"c", 4=>"d"}

The reason the first method isn't working, is because inject uses the result of the block as the r in the next iteration. For the first iteration, r is set to the argument you pass to it, which in this case is {}.

1 Comment

+1; doesn't explain anything, but is a better way (if example code is really what they're doing)--part two of the question.
4

The first block returns the result of the assignment back into the inject, the second returns the hash, so it actually works.

A lot of people consider this usage of inject an anti-pattern; consider each_with_object instead.

3 Comments

@Chuck Correct, if the OP's code is exactly what they're trying to do, I agree. I was answering the first part.
Personally I don't think that this is an anti-pattern for a fold. If explicitly having to return the hash bothers you, just use a different method: a.inject({}) { |r, (k, v)| r.merge(k => v) } (or merge! if the throwaway hashes bother you).
@MichaelKohl I didn't say I did... although if I was forced to take a side, I'd be against it on the elegance principle.
3
a.inject({}) { |r, val| r.merge({ val[0] => val[1] }) }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.